@jonathanyee/devtunnel-ctl 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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -2
  3. package/dist/auth.d.ts +23 -0
  4. package/dist/auth.js +150 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +206 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +94 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/core.d.ts +16 -0
  13. package/dist/core.js +112 -0
  14. package/dist/core.js.map +1 -0
  15. package/dist/daemon.d.ts +31 -0
  16. package/dist/daemon.js +153 -0
  17. package/dist/daemon.js.map +1 -0
  18. package/dist/forwarding.d.ts +15 -0
  19. package/dist/forwarding.js +87 -0
  20. package/dist/forwarding.js.map +1 -0
  21. package/dist/launchd.d.ts +10 -0
  22. package/dist/launchd.js +131 -0
  23. package/dist/launchd.js.map +1 -0
  24. package/dist/lock.d.ts +10 -0
  25. package/dist/lock.js +110 -0
  26. package/dist/lock.js.map +1 -0
  27. package/dist/log.d.ts +26 -0
  28. package/dist/log.js +67 -0
  29. package/dist/log.js.map +1 -0
  30. package/dist/main.d.ts +2 -0
  31. package/dist/main.js +5 -0
  32. package/dist/main.js.map +1 -0
  33. package/dist/paths.d.ts +4 -0
  34. package/dist/paths.js +39 -0
  35. package/dist/paths.js.map +1 -0
  36. package/dist/probe.d.ts +1 -0
  37. package/dist/probe.js +45 -0
  38. package/dist/probe.js.map +1 -0
  39. package/dist/recovery.d.ts +77 -0
  40. package/dist/recovery.js +265 -0
  41. package/dist/recovery.js.map +1 -0
  42. package/dist/rpc.d.ts +30 -0
  43. package/dist/rpc.js +208 -0
  44. package/dist/rpc.js.map +1 -0
  45. package/dist/sdk-transport.d.ts +29 -0
  46. package/dist/sdk-transport.js +239 -0
  47. package/dist/sdk-transport.js.map +1 -0
  48. package/npm-shrinkwrap.json +809 -0
  49. package/package.json +31 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jonyee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,282 @@
1
- # Temporary Holding Version
1
+ # devtunnel-ctl
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A per-user background service and CLI for **SSH-only** Microsoft Dev Tunnels forwarding. It uses the Microsoft TypeScript SDK directly, owns stable loopback ports, checks real SSH banners, and replaces stale SDK connections when bounded resumable recovery fails.
4
+
5
+ This is a first version, not a transparent SSH-session migration tool. A fresh SDK client can terminate existing SSH sessions. Use `tmux` on the remote machine when session continuity matters. No remote settings or SSH configuration are modified.
6
+
7
+ ## Installation
8
+
9
+ Requires Node.js 24 or later, npm, macOS or Linux, and an installed Microsoft `devtunnel` CLI. The authentication helper is not bundled. On macOS/Homebrew its default path is `/opt/homebrew/bin/devtunnel`; on Linux pass its actual absolute path to `init --helper`. The local daemon does not support Windows, but can connect to remote SSH endpoints running inside WSL.
10
+
11
+ ### From npm
12
+
13
+ After the first npm release:
14
+
15
+ ```sh
16
+ npm install --global --ignore-scripts @jonathanyee/devtunnel-ctl
17
+ devtunnel-ctl --help
18
+ devtunnel-ctl init --helper /opt/homebrew/bin/devtunnel
19
+ devtunnel-ctl daemon
20
+ ```
21
+
22
+ The package ships compiled JavaScript and a publishable dependency lock, so consumers do not need TypeScript or a source checkout. It works with dependency installation scripts disabled.
23
+
24
+ For a read-only global prefix, such as a Nix-managed Node installation, use a user-owned prefix instead of `sudo`:
25
+
26
+ ```sh
27
+ npm install --global --prefix "$HOME/.local" --ignore-scripts @jonathanyee/devtunnel-ctl
28
+ export PATH="$HOME/.local/bin:$PATH"
29
+ ```
30
+
31
+ Keep that bin directory on your shell's PATH. Switching an existing launchd service from a checkout to an npm installation changes its entrypoint: stop/uninstall the old service, then install/start it using the new command. Restart a daemon after package updates; reinstall its service if pinned runtime/helper paths change.
32
+
33
+ ### From source
34
+
35
+ ```sh
36
+ npm ci --ignore-scripts
37
+ npm run build
38
+ node dist/main.js --help
39
+ node dist/main.js init --helper /opt/homebrew/bin/devtunnel
40
+ node dist/main.js daemon
41
+ ```
42
+
43
+ The last command stays in the foreground. In another terminal:
44
+
45
+ ```sh
46
+ node dist/main.js profile add work \
47
+ --tunnel example-ssh.usw2 --local 23022 --remote 22
48
+ node dist/main.js status
49
+ ssh -p 23022 remote-user@127.0.0.1
50
+ ```
51
+
52
+ The names, tunnel ID and port above are fictitious examples. Choose an existing tunnel and an **unused** local port for initial verification. Do not stop or replace working forwards until you deliberately migrate them.
53
+
54
+ To expose the checkout as a command after building:
55
+
56
+ ```sh
57
+ npm install --global --prefix "$HOME/.local" --ignore-scripts "$PWD"
58
+ export PATH="$HOME/.local/bin:$PATH"
59
+ ```
60
+
61
+ This local installation remains tied to the checkout. Otherwise use `node /absolute/path/to/devtunnel-ctl/dist/main.js` throughout.
62
+
63
+ ## Profiles and commands
64
+
65
+ Each profile maps one remote SSH port to one fixed local port. Both `127.0.0.1:PORT` and `[::1]:PORT` are bound exclusively; there is no LAN listener and no automatic port shifting. A conflict on either address fails that profile explicitly. An IPv6-disabled environment must enable IPv6 loopback before using this version.
66
+
67
+ ```sh
68
+ devtunnel-ctl profile add work --tunnel example-ssh \
69
+ --cluster usw2 --local 23022 --remote 22 --paused
70
+ devtunnel-ctl profile list
71
+ devtunnel-ctl status work
72
+ devtunnel-ctl status --json
73
+ devtunnel-ctl resume work
74
+ devtunnel-ctl pause work
75
+ devtunnel-ctl reconnect work
76
+ devtunnel-ctl reconnect all
77
+ devtunnel-ctl logs
78
+ devtunnel-ctl logs --follow
79
+ ```
80
+
81
+ Pause is persisted before acknowledging the command and survives daemon restart. Pause releases that profile's listeners and connections. **Reconnect never unpauses a profile.** Resume and reconnect initiate work asynchronously; consult status for the actual result, rather than treating the command acknowledgement as proof of connectivity.
82
+
83
+ Profile names and local ports must be unique; at most 64 profiles are supported. `--remote` defaults to 22 but may identify another SSH server port. Non-SSH services and other ports exposed by the tunnel are not forwarded. To change or remove an existing profile, stop the daemon, edit its private `config.json`, and restart it. Do not edit configuration while the daemon runs.
84
+
85
+ ### Existing SSH aliases
86
+
87
+ Existing aliases remain unchanged when the same local port is assigned to a profile. Migration is explicit: first verify an isolated spare port, then stop the old forward yourself and configure the desired stable port. An occupied port is reported as `port-conflict`; the service never kills the current owner or silently chooses a different port.
88
+
89
+ ## Authentication
90
+
91
+ The SDK performs connection and forwarding work. The Microsoft CLI is used **only** for explicit sign-in and acquiring short-lived, connect-scoped tunnel tokens:
92
+
93
+ ```sh
94
+ devtunnel-ctl login
95
+ # Optional explicit alternatives:
96
+ devtunnel-ctl login --device-code
97
+ devtunnel-ctl login --github
98
+ devtunnel-ctl reconnect all
99
+ ```
100
+
101
+ The service never invokes login, reads a credential cache, or uses a copied application ID. It runs the absolute helper executable with an argument array equivalent to `devtunnel token ID --scopes connect --json`, capturing stdout/stderr privately with a 12-second deadline and 64 KiB combined limit. Raw helper output and tokens are never printed or logged. There is no shell interpolation.
102
+
103
+ Tokens are coalesced and cached **in memory only**, with expiration checks and early renewal. On management 401, one fresh token is tried before surfacing sign-in required; SDK relay token refresh is similarly bounded. If relay authentication triggers token refresh but acquisition fails transiently, the sanitized refresh failure remains retryable even when the SDK wraps it in the original 401, including asynchronous reconnect events. Genuine rejection after a successful refresh still requires user action. Refresh evidence is scoped to its connection episode, and obsolete completions cannot overwrite a newer episode's classification. Reconnect invalidates that profile's cached token. Tunnel tokens currently normally expire after 24 hours; identity sign-in can also expire or be revoked. This does not provide unattended identity renewal beyond what the signed-in CLI can acquire.
104
+
105
+ The parser accepts a JSON string containing a JWT or an object with a `token` JWT field; unsupported output fails safely without exposing it. The installed CLI's real envelope was privately verified, including its `token` field and unrelated metadata. Token-only SDK metadata reads and a complete live SSH forwarding connection were also exercised.
106
+
107
+ Specify the cluster explicitly (`ID.CLUSTER` or `--cluster`) for predictable routing. For an unqualified ID, the currently issued token's validated-format `clusterId` claim is used only as a routing hint, not as proof of authorization. If that hint is absent, status requests an explicit cluster rather than guessing. TLS and SDK host-key checks remain enabled; endpoint metadata without published host public keys is rejected, including during refresh. The SDK's default E2E encryption request is preserved.
108
+
109
+ ## Recovery policy
110
+
111
+ | Signal or condition | Behavior |
112
+ | --- | --- |
113
+ | Relay connected | Not sufficient for `healthy`; a local forwarding connection must receive a valid SSH banner. |
114
+ | Normal operation | SSH banner check every 10 seconds, with a 3-second check deadline; SDK keepalive interval is 10 seconds. |
115
+ | SDK disconnection | Allow up to 8 seconds for resumable SDK reconnect, then cancel/dispose and replace the generation. |
116
+ | Failed SSH check with apparently connected relay | Report `Tunnel: connected; SSH: no response. Check remote SSH/WSL.` in status and logs, recheck after 5 seconds, then replace a persistently stale client. WSL is a troubleshooting hint, not a detected cause; a broken forwarding path and an unavailable remote SSH server cannot always be distinguished. |
117
+ | Sleep or long scheduling gap | A gap over 15 seconds prompts re-evaluation. A passing SSH check keeps the healthy client rather than blindly replacing it. |
118
+ | Failed fresh connection | Independent per-profile exponential backoff, 50–100% jitter, capped at 15 seconds. Respect exposed Retry-After up to 24 hours; no automatic bypass on reconnect. |
119
+ | Successful end-to-end check | Update `lastHealthyAt` and reset retry backoff. |
120
+ | Sign-in required, access denied, invalid configuration | Stop automated attempts for that profile and report the required action. Explicit reconnect retries after the user addresses it. |
121
+ | Port conflict, absent host, unavailable path, network errors | Continue bounded unattended retry. One failed target does not establish that Wi-Fi or the internet is offline. |
122
+
123
+ The outer controller owns long-term scheduling. SDK retry events allow only the bounded resumable window and a single initial token-refresh retry; there are no competing unbounded retry loops.
124
+
125
+ A fresh connection generation has a 25-second overall deadline. SDK connect, metadata refresh, forwarded-port wait, stream opening, helper execution and disposal have their own deadlines and cancellation. Obsolete results cannot become the current client; late streams are destroyed and late clients disposed. Pending SDK WebSocket sockets are owned by a per-generation HTTPS agent and destroyed when cancelled. Failed or slow teardown does not indefinitely block explicit reconnect, pause, or another profile.
126
+
127
+ Listeners stay bound across SDK replacements. New connections fail immediately while no connected SDK client is available, or within 3 seconds when opening a forwarded stream stalls; they are not queued indefinitely. Existing streams may survive the short SDK resumable window, but a fresh-client replacement breaks them. There is a 256-connection-per-profile cap.
128
+
129
+ **Acceptance target:** restore forwarding within 60 seconds after connectivity returns when the host and authentication are available. Deterministic tests cover a sustained outage with maximum jitter/backoff, a 24-second fresh connection and a 2-second SSH probe. This is not a live SLA: provider throttling, expired identity, unavailable hosts, slow authentication, and upstream SDK behavior may exceed it. Actual post-sleep/offline recovery has not yet been verified against a live tunnel.
130
+
131
+ ## Optional macOS login service
132
+
133
+ Installation is explicit and per user; never use `sudo`. Stop a foreground daemon before installing:
134
+
135
+ ```sh
136
+ devtunnel-ctl service install
137
+ devtunnel-ctl service start
138
+ devtunnel-ctl service status
139
+ devtunnel-ctl status
140
+ devtunnel-ctl service stop
141
+ devtunnel-ctl service uninstall
142
+ ```
143
+
144
+ Install writes a private plist under `~/Library/LaunchAgents/` with a state-directory-specific label. **Install does not start the service.** Start bootstraps it into the current user's GUI domain; stop bootouts it. launchd restarts an unexpectedly exited daemon, with a 15-second launch throttle. No agent has been installed or started as part of development.
145
+
146
+ Installation resolves and pins absolute Node, built entrypoint, helper, and state paths. It does not depend on login-shell PATH. Keep the project and pinned runtime available; stop/uninstall/build/reinstall after moving the project or changing/removing the runtime or helper. A stopped service retains profiles and logs. Status distinguishes launchd registration from an actually responsive daemon.
147
+
148
+ ## Private state and control
149
+
150
+ Default state is `~/.devtunnel-ctl/`. Use the same `--state-dir /absolute/path` on every command to select a separate instance. The full control-socket path must be at most 100 bytes.
151
+
152
+ ```json
153
+ {
154
+ "version": 1,
155
+ "helperPath": "/opt/homebrew/bin/devtunnel",
156
+ "profiles": [
157
+ {
158
+ "name": "work",
159
+ "tunnelId": "example-ssh",
160
+ "clusterId": "usw2",
161
+ "remotePort": 22,
162
+ "localPort": 23022,
163
+ "paused": true
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ Directories are private (0700); config, lock records, logs and socket are private (0600). Symlinks and foreign/insecure state ownership are refused. Configuration uses a unique temporary file, fsync and atomic rename. There are no tokens in configuration.
170
+
171
+ The management interface is a Unix-domain socket, not HTTP. Requests are validated, limited to 32 KiB and timed out; responses are capped at 128 KiB. An absent daemon is an error. A timed-out mutation reports uncertainty: check status before retrying because its atomic configuration write may already have completed.
172
+
173
+ Daemon exclusion uses atomically renamed populated lock directories with unique PID records. A demonstrably dead owner's record can be reclaimed without removing a new owner's record. A live owner is never replaced. Stale sockets are removed only under exclusive state ownership and after connection refusal; live or unresponsive sockets are never unlinked. A reused live PID or malformed lock record is conservatively refused, not guessed away.
174
+
175
+ Logs use safe fields and redaction instead of raw SDK traces. Two rotating files (`service.log` and `service.log.1`) are limited to 256 KiB each; the current daemon retains at most 128 recent entries for `logs --follow`. Log queue growth is bounded; failure to maintain private logs stops the service with an explicit error. Logs from previous runs remain on disk; the CLI shows the current daemon's recent in-memory entries.
176
+
177
+ ## Troubleshooting and live verification
178
+
179
+ | Symptom | Action |
180
+ | --- | --- |
181
+ | `daemon-unavailable` | Start `daemon` in a terminal, or `service start` if installed. Use the same state directory. |
182
+ | `port-conflict` | Inspect/release the occupied port yourself, or stop the daemon and edit the profile to use an unused port. Both IPv4 and IPv6 must be available. |
183
+ | `login-required` | Explicitly run `login`, then `reconnect NAME` or `reconnect all`. |
184
+ | `access-denied` | Check account and tunnel ACLs; signing in repeatedly is not assumed to solve permission errors. |
185
+ | `remote-unavailable` with relay connected | Confirm remote SSH service/port and host health, including WSL if used. A connected relay or open local port is not end-to-end SSH success. |
186
+ | Service loaded but unresponsive | Stop it and run `daemon` in a terminal to see sanitized startup errors. Check absolute pinned paths and private state permissions. |
187
+ | Lock refused after crash | PID reuse or malformed/foreign lock contents require manual inspection. Do not delete a lock until you have established there is no live daemon. |
188
+ | Unexpected SSH disconnect | Necessary fresh-client replacement may drop old sessions; reconnect and reattach `tmux`. |
189
+
190
+ For an isolated live check, coordinate first so no existing tunnel session is migrated or disrupted:
191
+
192
+ 1. Build, create a separate private state directory, and initialize it with the installed helper.
193
+ 2. Start a foreground daemon using that state directory. Add one existing tunnel's SSH endpoint on a separate **unused** loopback port.
194
+ 3. Observe `status --json` until `healthy`; verify the real SSH banner or make an SSH connection on the spare port. This verifies fresh-token acquisition, management metadata, relay transport, host-key checks, and the SSH forwarding path together.
195
+ 4. Exercise explicit reconnect and confirm the same local port recovers. Do not toggle networking or put a shared Mac to sleep for testing without approval. Observe a naturally occurring interruption separately and measure from connectivity return to `lastHealthyAt`.
196
+ 5. Stop that verification daemon and remove only its isolated state after inspecting it. Do not change the original working forwards or SSH aliases.
197
+
198
+ The implementation has been checked against installed SDK 1.3.61 declarations/source, a real management SDK with isolated HTTP adapters, and the actual SDK WebSocket handshake cancellation path against a local stalled TCP peer. Automated daemon/CLI end-to-end tests use a deliberately injected fake transport and local SSH-banner server, not a production fallback.
199
+
200
+ Additional coordinated live checks used private in-memory CLI token acquisition, an unqualified tunnel ID resolved through SDK metadata, and a separate unused loopback port. They verified real SSH banners, the real SDK endpoint-refresh hook, and controlled CLI reconnect to a second client with the original client disposed (about 2 seconds). Disposing only the isolated test SDK client also exercised **automatic** grace/backoff/replacement recovery to a fresh SSH-banner-verified channel in 10.858 seconds, without issuing reconnect.
201
+
202
+ These are controlled checks, **not** proof of recovery after Mac sleep or of the real-world 60-second target. One additional live run reached its 45-second whole-check deadline without a definitive cause captured; subsequent staged checks and the controlled automatic recovery check passed. Unattended field reliability is not yet established. An initially unavailable target remained unhealthy despite a connected relay, matching the original forward's lack of an SSH banner. Temporary verification state and daemons were removed; existing forwards were untouched. No login or launchd registration was performed.
203
+
204
+ ## Development
205
+
206
+ ```sh
207
+ npm run typecheck
208
+ npm run build
209
+ npm test
210
+ ```
211
+
212
+ Tests use Node's built-in runner and type stripping. They cover private configuration and helper handling, actual dual-loopback binds/conflicts, forwarding backpressure and half-close, queued-write draining after remote EOF, abrupt remote closure, SSH banners, fake-clock recovery/deadlines/long gaps, stale async completions, authentication/endpoint refresh, redaction/rotation, malformed and oversized IPC, concurrent daemon locking, and real CLI/daemon subprocess restart flows. Large-response assertions compare byte counts and equality without rendering multi-megabyte buffer diffs. All verification daemons are isolated and shut down by their tests.
213
+
214
+ ## Package verification and publishing
215
+
216
+ The npm package is `@jonathanyee/devtunnel-ctl`; the installed command remains `devtunnel-ctl`. The project is MIT-licensed; see [LICENSE](LICENSE).
217
+
218
+ ### Verify the actual artifact
219
+
220
+ ```sh
221
+ npm ci --ignore-scripts
222
+ npm run typecheck
223
+ npm test
224
+ npm run package:check -- --expected-version 0.1.0
225
+ npm run package:check -- --verify-only --expected-version 0.1.0
226
+ ```
227
+
228
+ `prepack` builds the compiled files before every pack. The package checker:
229
+
230
+ - Allows only `dist`, package metadata, README, license and `npm-shrinkwrap.json`; it rejects source, tests, scripts, workflows and unexpected files.
231
+ - Installs the tarball into an isolated temporary global prefix, without development dependencies, credentials or installation scripts.
232
+ - Runs the installed command outside the checkout, verifies its runtime dependencies are inside that prefix, and exercises an isolated daemon/status/paused-profile flow without connecting to real tunnels.
233
+ - Stops that daemon and removes temporary installation/state directories.
234
+ - Writes the checked tarball and `result.json` under ignored `.artifacts/`. The result includes SHA-512 integrity; `--verify-only` rejects incomplete reports or modified artifacts.
235
+
236
+ The consumer installation **prefers cached data**, but downloads missing metadata or dependencies from your currently configured npm registry, including a locally configured mirror. `npm ci` alone does not cache all metadata needed for a global install. The smoke check never silently falls back to another registry. The integration test uses an empty public-npm cache to cover fresh-runner installation. A successful local cached install is not proof that a fresh public-registry download works from the current network.
237
+
238
+ The source lock is `npm-shrinkwrap.json`, not `package-lock.json`, because this is a globally installed application. It is included in the published tarball to preserve tested transitive versions and integrity hashes. Keep its tarball origins public; do not commit machine-specific npm configuration or authentication.
239
+
240
+ Use a fresh output directory after changing versions, or remove only the prior generated artifacts yourself. The checker refuses unrelated files in its output directory.
241
+
242
+ ### First publication: owner-controlled bootstrap
243
+
244
+ Preparation does **not** publish the package or reserve an npm name. Merge the reviewed packaging changes into `main` before the first release. npm trusted publishers are configured in an existing package's settings, so a new package may need its first publication performed interactively before that connection can be added.
245
+
246
+ When ready, on a trusted machine with public npm access:
247
+
248
+ ```sh
249
+ npm login --registry=https://registry.npmjs.org/
250
+ npm whoami --registry=https://registry.npmjs.org/
251
+ ```
252
+
253
+ Confirm the account is `jonathanyee`, with verified email and two-factor authentication. Check that the intended version has not already been published. Network/authentication errors are not evidence that a name or version is available.
254
+
255
+ After running the artifact checks above, these commands perform the **actual first publication**:
256
+
257
+ ```sh
258
+ artifact="$(node -p "require('./.artifacts/result.json').filename")"
259
+ npm publish ".artifacts/$artifact" --ignore-scripts --access public --registry=https://registry.npmjs.org/
260
+ ```
261
+
262
+ Do not paste tokens into chat or add them to the repository. The explicit registry and package `publishConfig` prevent relying on a machine's default publishing registry. If a publish result is uncertain, check npm before retrying; an existing version cannot be republished.
263
+
264
+ ### Subsequent publications: manual GitHub Actions with OIDC
265
+
266
+ Before the next version is ready to publish:
267
+
268
+ 1. In this GitHub repository, configure the `npm` environment with required approval and restrict deployments to `main`.
269
+ 2. In npm package settings, add a GitHub Actions trusted publisher: GitHub owner **`jonyee`**, repository **`devtunnel-ctl`**, workflow **`publish.yml`**, environment **`npm`**. The GitHub owner and npm scope are different names.
270
+ 3. Configure publishing access to require two-factor authentication and disallow traditional tokens after verifying the trusted publisher works.
271
+
272
+ A newly created trusted-publisher connection must complete its first successful publish within npm's validation window (currently two days); configure it when the next release is ready, or recreate an expired connection.
273
+
274
+ For a new version, update both the manifest and shrinkwrap, review and merge the change:
275
+
276
+ ```sh
277
+ npm version patch --no-git-tag-version
278
+ ```
279
+
280
+ Then select **Actions → Publish npm → Run workflow**, choose `main`, and enter the exact version in `package.json`. The workflow rejects other branches or mismatched versions, runs all checks, install-tests a fresh tarball, verifies its integrity again, and publishes that exact tarball using OIDC. It uses GitHub-hosted runners, an OIDC-capable pinned npm, and no stored publishing token. npm generates provenance automatically for trusted publication from this public repository.
281
+
282
+ There are no push/tag/release triggers for publication. Tags and GitHub Releases remain separate, explicit owner actions. Neither package verification nor CI checks register launchd, log in, or alter existing tunnels.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface Token {
2
+ value: string;
3
+ expiresAt: number;
4
+ clusterId?: string;
5
+ }
6
+ export declare function parseToken(output: string): Token;
7
+ export declare function runHelper(path: string, args: string[], signal?: AbortSignal, timeoutMs?: number): Promise<string>;
8
+ type Runner = (path: string, args: string[], signal?: AbortSignal) => Promise<string>;
9
+ type Reference = {
10
+ tunnelId: string;
11
+ clusterId?: string;
12
+ };
13
+ export declare class TokenProvider {
14
+ private readonly cache;
15
+ private readonly flights;
16
+ readonly path: string;
17
+ private readonly runner;
18
+ constructor(path: string, runner?: Runner);
19
+ get(ref: Reference, signal?: AbortSignal, refresh?: boolean): Promise<Token>;
20
+ clear(): void;
21
+ invalidate(ref: Reference): void;
22
+ }
23
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,150 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { bounded, Fault, record } from "./core.js";
3
+ export function parseToken(output) {
4
+ let value;
5
+ try {
6
+ value = JSON.parse(output);
7
+ }
8
+ catch {
9
+ throw new Fault('protocol', 'Token helper returned invalid JSON.');
10
+ }
11
+ const token = typeof value === 'string' ? value : record(value) ? value.token : undefined;
12
+ if (typeof token !== 'string' ||
13
+ !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) {
14
+ throw new Fault('protocol', 'Token helper returned an unsupported token envelope.');
15
+ }
16
+ let claims;
17
+ try {
18
+ claims = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
19
+ }
20
+ catch {
21
+ throw new Fault('protocol', 'Token helper returned an invalid token.');
22
+ }
23
+ if (!record(claims) || typeof claims.exp !== 'number' || !Number.isSafeInteger(claims.exp) ||
24
+ !Number.isFinite(claims.exp * 1000)) {
25
+ throw new Fault('protocol', 'Token has no usable expiration.');
26
+ }
27
+ const expiresAt = claims.exp * 1000;
28
+ if (expiresAt <= Date.now() + 30000)
29
+ throw new Fault('login-required', 'Acquired token is expired or about to expire. Run devtunnel-ctl login.');
30
+ const clusterId = typeof claims.clusterId === 'string' && /^[a-z0-9][a-z0-9-]{0,62}$/.test(claims.clusterId)
31
+ ? claims.clusterId : undefined;
32
+ return { value: token, expiresAt, ...(clusterId ? { clusterId } : {}) };
33
+ }
34
+ export function runHelper(path, args, signal, timeoutMs = 12000) {
35
+ return bounded(inner => new Promise((resolve, reject) => {
36
+ if (inner.aborted) {
37
+ reject(new Fault('aborted'));
38
+ return;
39
+ }
40
+ const child = spawn(path, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: false,
41
+ env: { ...process.env, CI: 'true' } });
42
+ let stdout = '', stderr = '', bytes = 0, finished = false;
43
+ const stop = () => {
44
+ child.kill('SIGTERM');
45
+ const kill = setTimeout(() => { if (child.exitCode === null)
46
+ child.kill('SIGKILL'); }, 200);
47
+ kill.unref();
48
+ child.once('close', () => clearTimeout(kill));
49
+ };
50
+ const abort = () => { stop(); complete(new Fault('aborted')); };
51
+ const complete = (error) => {
52
+ if (finished)
53
+ return;
54
+ finished = true;
55
+ inner.removeEventListener('abort', abort);
56
+ if (error)
57
+ reject(error);
58
+ else
59
+ resolve(stdout);
60
+ };
61
+ inner.addEventListener('abort', abort, { once: true });
62
+ const data = (chunk, out) => {
63
+ if (finished)
64
+ return;
65
+ bytes += chunk.length;
66
+ if (bytes > 65536) {
67
+ stop();
68
+ complete(new Fault('protocol', 'Token helper output exceeded the private capture limit.'));
69
+ }
70
+ else if (out)
71
+ stdout += chunk.toString();
72
+ else
73
+ stderr += chunk.toString();
74
+ };
75
+ child.stdout.on('data', chunk => data(chunk, true));
76
+ child.stderr.on('data', chunk => data(chunk, false));
77
+ child.on('error', () => complete(new Fault('invalid-config', 'Cannot execute configured devtunnel helper. Check its absolute path.')));
78
+ child.on('close', code => {
79
+ if (code === 0)
80
+ complete();
81
+ else if (/sign.?in|log.?in|not authenticated|unauthorized|401/i.test(stderr))
82
+ complete(new Fault('login-required'));
83
+ else if (/forbidden|access.?denied|403/i.test(stderr))
84
+ complete(new Fault('access-denied'));
85
+ else if (/429|throttl|too many requests/i.test(stderr))
86
+ complete(new Fault('throttled', undefined, 30000));
87
+ else
88
+ complete(new Fault('retrying', 'Token acquisition failed; recovery will retry. No automatic sign-in is attempted.'));
89
+ stdout = '';
90
+ stderr = '';
91
+ });
92
+ }), timeoutMs, signal);
93
+ }
94
+ export class TokenProvider {
95
+ cache = new Map();
96
+ flights = new Map();
97
+ path;
98
+ runner;
99
+ constructor(path, runner = runHelper) { this.path = path; this.runner = runner; }
100
+ async get(ref, signal, refresh = false) {
101
+ const key = ref.tunnelId + (ref.clusterId ? `.${ref.clusterId}` : '');
102
+ if (signal?.aborted)
103
+ throw new Fault('aborted');
104
+ if (refresh)
105
+ this.cache.delete(key);
106
+ const cached = this.cache.get(key);
107
+ if (cached && cached.expiresAt > Date.now() + 60000)
108
+ return cached;
109
+ let flight = this.flights.get(key);
110
+ if (flight?.controller.signal.aborted) {
111
+ this.flights.delete(key);
112
+ flight = undefined;
113
+ }
114
+ if (!flight) {
115
+ const controller = new AbortController();
116
+ const promise = this.runner(this.path, ['token', key, '--scopes', 'connect', '--json'], controller.signal)
117
+ .then(output => {
118
+ if (controller.signal.aborted)
119
+ throw new Fault('aborted');
120
+ const token = parseToken(output);
121
+ this.cache.set(key, token);
122
+ return token;
123
+ });
124
+ flight = { promise, controller, users: 0 };
125
+ this.flights.set(key, flight);
126
+ const current = flight;
127
+ void promise.finally(() => { if (this.flights.get(key) === current)
128
+ this.flights.delete(key); }).catch(() => { });
129
+ }
130
+ flight.users++;
131
+ try {
132
+ return await bounded(() => flight.promise, 13000, signal);
133
+ }
134
+ finally {
135
+ if (--flight.users === 0)
136
+ flight.controller.abort();
137
+ }
138
+ }
139
+ clear() {
140
+ this.cache.clear();
141
+ for (const flight of this.flights.values())
142
+ flight.controller.abort();
143
+ this.flights.clear();
144
+ }
145
+ invalidate(ref) {
146
+ const key = ref.tunnelId + (ref.clusterId ? `.${ref.clusterId}` : '');
147
+ this.cache.delete(key);
148
+ }
149
+ }
150
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const help = "devtunnel-ctl - resilient SSH-only Dev Tunnels forwarding\n\n init [--helper /absolute/path/devtunnel]\n daemon Foreground service; Ctrl-C stops it\n profile add NAME --tunnel ID[.CLUSTER] --local PORT [--remote 22] [--cluster ID] [--paused]\n profile list | status [NAME] [--json]\n pause NAME|all | resume NAME|all | reconnect NAME|all\n logs [--follow] Recent sanitized logs from the running daemon\n login [--device-code] [--github] Explicit devtunnel sign-in only\n service install|start|stop|uninstall|status\n\nGlobal: --state-dir /absolute/private/directory, --help\nState defaults to ~/.devtunnel-ctl. Start a daemon before modifying profiles.\nReconnect never unpauses. Stable ports never shift. Fresh clients may break SSH sessions.\n";
2
+ export declare function cli(args: string[], entry: string): Promise<void>;
3
+ export declare function report(error: unknown): void;