@lawaty/lacode 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -12
- package/docs/TUNNEL.md +67 -116
- package/docs/USER-STORIES.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,17 +213,15 @@ Three things differ, and all three are worth reading rather than assuming:
|
|
|
213
213
|
published package. They drive one person's SSH topology, so each is on the side
|
|
214
214
|
it belongs to:
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
cannot work and says which `lacode` subcommand to use instead, rather than
|
|
226
|
-
failing somewhere less obvious.
|
|
216
|
+
- **Remote hosts are plain npm clients.** A remote host installs the plugin the
|
|
217
|
+
same way (`opencode plugin @lawaty/lacode -g`); there is nothing to rsync. The
|
|
218
|
+
notification hop is a direct ssh (`ControlMaster` + `ControlPersist`) set up
|
|
219
|
+
once with `lacode tunnel setup` — no reverse forward, no listening port, no
|
|
220
|
+
systemd unit. See [Remote hosts](#remote-hosts) and docs/TUNNEL.md.
|
|
221
|
+
- The repo still carries the author's personal, Linux-only tooling from the older
|
|
222
|
+
reverse-forward setup (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
|
|
223
|
+
`bin/oc-sync`). None of it is in the published package, and none of it is needed
|
|
224
|
+
for the npm-client path above.
|
|
227
225
|
|
|
228
226
|
**The test suite runs on macOS.** It is written to pass on a Mac as well as on
|
|
229
227
|
Linux, so "it works on a Mac" is a supported claim rather than an intention. The
|
|
@@ -233,7 +231,7 @@ or no GNU `timeout` — rather than passing silently or failing confusingly.
|
|
|
233
231
|
|
|
234
232
|
### The `lacode` CLI
|
|
235
233
|
|
|
236
|
-
The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`) — see
|
|
234
|
+
The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
|
|
237
235
|
[Remote hosts](#remote-hosts). The plugin install does **not** put it on your
|
|
238
236
|
`PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
|
|
239
237
|
globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
|
package/docs/TUNNEL.md
CHANGED
|
@@ -9,31 +9,23 @@ gets it back.
|
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
remote host your desktop
|
|
12
|
-
┌──────────────────┐
|
|
13
|
-
│ opencode session │
|
|
14
|
-
│ │ │
|
|
15
|
-
│ ▼ │
|
|
16
|
-
│
|
|
17
|
-
│
|
|
18
|
-
│ │
|
|
12
|
+
┌──────────────────┐ direct ssh ┌────────────────────────┐
|
|
13
|
+
│ opencode session │ (ControlPersist) │ lacode receive │
|
|
14
|
+
│ │ │ ───────────────────────► │ (authorized_keys │
|
|
15
|
+
│ ▼ │ ONE argument: │ forced command) │
|
|
16
|
+
│ lacode notify │ title\nmessage\n │ │ │
|
|
17
|
+
│ │ urgency\nicon │ ▼ │
|
|
18
|
+
│ │ │ notify-send / │
|
|
19
|
+
│ │ │ osascript │
|
|
19
20
|
└──────────────────┘ └────────────────────────┘
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
**Direct ssh
|
|
23
|
-
|
|
24
|
-
connection
|
|
25
|
-
port, no systemd unit and nothing to sync: the plugin owns the
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`OC_LOCAL_HOST` is unset.
|
|
29
|
-
|
|
30
|
-
**Reverse forward (legacy).** Two things are load-bearing here.
|
|
31
|
-
|
|
32
|
-
**The reverse forward.** The remote opens an SSH connection to *your* machine
|
|
33
|
-
and asks sshd on the desktop to listen on a port. A notification is then an ssh
|
|
34
|
-
call to `localhost:<port>` with a dedicated key that is authorized to run
|
|
35
|
-
exactly one command. It is not a login, it is not a shell, and it does not
|
|
36
|
-
depend on which terminal connected first.
|
|
23
|
+
**Direct ssh, one warm connection.** The hop is a plain `ssh` to the desktop with
|
|
24
|
+
`ControlMaster=auto` + `ControlPersist`, so the first notification opens the
|
|
25
|
+
connection and every later one rides it. There is no reverse forward, no
|
|
26
|
+
listening port, no systemd unit and nothing to sync: the plugin owns the
|
|
27
|
+
transport, and it exists only while opencode runs — which is the only time a
|
|
28
|
+
notification matters.
|
|
37
29
|
|
|
38
30
|
**One argument, not four.** ssh concatenates everything after the host into a
|
|
39
31
|
single command string, and a forced command starts with *no* positional
|
|
@@ -51,17 +43,46 @@ the free-text fields before assembling or reading it, and `urgency` and `icon`
|
|
|
51
43
|
come from whitelists. No field can contain the delimiter, so a message full of
|
|
52
44
|
spaces, quotes or `*` survives intact.
|
|
53
45
|
|
|
46
|
+
## Setting it up
|
|
47
|
+
|
|
48
|
+
Once, on the **remote host** (where opencode runs):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
lacode tunnel setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It generates the dedicated key (`~/.ssh/lacode_notify` by default) and prints the
|
|
55
|
+
environment to export there, and the single `authorized_keys` line to add on the
|
|
56
|
+
desktop:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
OC_LOCAL_USER=<desktop user>
|
|
60
|
+
OC_LOCAL_HOST=<desktop hostname>
|
|
61
|
+
OC_NOTIFY_KEY=~/.ssh/lacode_notify
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
On the **desktop**, add the printed line to `~/.ssh/authorized_keys`:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
restrict,command="lacode receive" ssh-ed25519 AAAA… lacode-notify
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`restrict` does the security work: no shell, no PTY, no agent forwarding, no port
|
|
71
|
+
forwarding. The key can do exactly one thing — hand one notification to
|
|
72
|
+
`lacode receive` — and nothing else. Use the absolute path to `lacode` if it is
|
|
73
|
+
not on the forced command's `PATH`.
|
|
74
|
+
|
|
54
75
|
## Sending
|
|
55
76
|
|
|
56
77
|
The CLI picks the transport by where it is running — `SSH_CLIENT` or `SSH_TTY`
|
|
57
78
|
means remote, so it forwards; otherwise it delivers locally through whichever
|
|
58
|
-
desktop notifier this platform has: `notify-send`
|
|
59
|
-
|
|
79
|
+
desktop notifier this platform has: `notify-send` on Linux, `osascript` on macOS,
|
|
80
|
+
with `terminal-notifier` as the macOS fallback when `osascript` is missing.
|
|
60
81
|
|
|
61
82
|
```bash
|
|
62
|
-
lacode notify "build finished" "
|
|
83
|
+
lacode notify "build finished" "4 files changed" normal dialog-information
|
|
63
84
|
lacode notify "question" "which database should I use?" critical dialog-warning
|
|
64
|
-
lacode forward web-01 -- systemctl --user status
|
|
85
|
+
lacode forward web-01 -- systemctl --user status something
|
|
65
86
|
lacode doctor
|
|
66
87
|
```
|
|
67
88
|
|
|
@@ -76,108 +97,38 @@ handler, never spliced into the script source — a session title is not trusted
|
|
|
76
97
|
input. `lacode doctor` prints the channel that would actually run, so it is
|
|
77
98
|
useful on a Mac rather than reporting only whether a Linux binary is installed.
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
## The desktop side: `lacode receive`
|
|
101
|
+
|
|
102
|
+
`lacode receive` is the forced command. It reads the newline payload from
|
|
103
|
+
`$SSH_ORIGINAL_COMMAND`, sanitizes each field again at the trust boundary, and
|
|
104
|
+
delivers locally through the platform channel — `notify-send` on Linux,
|
|
105
|
+
`osascript`/`terminal-notifier` on macOS. Because it delegates to the same local
|
|
106
|
+
channel as a local send, **the desktop half is not Linux-only**: a Mac desktop
|
|
107
|
+
receives notifications too.
|
|
108
|
+
|
|
109
|
+
It never forwards: `SSH_CLIENT` is set for a forced command as well, so it calls
|
|
110
|
+
the local channel directly rather than `notify()` (which would loop back).
|
|
80
111
|
|
|
81
112
|
## Configuration
|
|
82
113
|
|
|
83
114
|
| Variable | Default | Meaning |
|
|
84
115
|
| --- | --- | --- |
|
|
85
116
|
| `OC_LOCAL_USER` | *(unset)* | The desktop user the hop authenticates as. **Required for the remote hop.** |
|
|
86
|
-
| `OC_LOCAL_HOST` | *(unset)* | Desktop hostname for the
|
|
87
|
-
| `OC_TUNNEL_PORT` | `10022` | Reverse-forward listener port on the desktop. Give each remote host its own. |
|
|
117
|
+
| `OC_LOCAL_HOST` | *(unset)* | Desktop hostname for the direct ssh hop. The default; set it and no reverse forward or port is used. |
|
|
88
118
|
| `OC_NOTIFY_KEY` | `~/.ssh/lacode_notify` | The dedicated key. Never a personal key. |
|
|
89
119
|
| `OC_CONNECT_TIMEOUT_MS` | `5000` | Ceiling on the hop. |
|
|
90
120
|
| `OC_SSH_TIMEOUT_MS` | `30000` | Ceiling on `forward`. |
|
|
121
|
+
| `OC_TUNNEL_PORT` | `10022` | **Legacy** reverse-forward listener port. Used only when `OC_LOCAL_HOST` is unset. |
|
|
91
122
|
|
|
92
123
|
`OC_LOCAL_USER` is read rather than probed on purpose. `id -un` on a remote
|
|
93
124
|
account frequently returns `root`, and a dbus path built from that reaches
|
|
94
125
|
nobody's desktop. Guessing the desktop user is worse than failing.
|
|
95
126
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
The receiver runs on the machine with the desktop, as an `authorized_keys`
|
|
99
|
-
forced command restricted to that one script. `$HOME` must be the expanded
|
|
100
|
-
absolute path — a `~` is not expanded there, and an unexpanded one produces an
|
|
101
|
-
`authorized_keys` entry that names a file that does not exist:
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
restrict,command="/home/yourname/.local/bin/lacode-notify-receiver" ssh-ed25519 AAAA… oc-notify tunnel auth
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
`restrict` is doing the security work: no shell, no PTY, no agent forwarding, no
|
|
108
|
-
port forwarding. The key can do exactly one thing and nothing else.
|
|
109
|
-
|
|
110
|
-
The receiver must satisfy this contract:
|
|
111
|
-
|
|
112
|
-
1. read the payload from **`$SSH_ORIGINAL_COMMAND`**, never from `$1`..`$4`;
|
|
113
|
-
2. **never** `eval` it, never hand it to `sh -c`, never splice it into an
|
|
114
|
-
argument list — it is attacker-controlled, because it is whatever the remote
|
|
115
|
-
user typed;
|
|
116
|
-
3. split it on newlines into title / message / urgency / icon, each with a
|
|
117
|
-
default — **using only constructs bash 3.2 has**, not just bash 4+;
|
|
118
|
-
4. whitelist `urgency` against `low|normal|critical` and `icon` against the six
|
|
119
|
-
supported names, both of which become command-line flags;
|
|
120
|
-
5. resolve `DISPLAY` / `DBUS_SESSION_BUS_ADDRESS` for the session it is running
|
|
121
|
-
in, then `exec notify-send "$TITLE" "$MSG" --urgency=… --icon=…`.
|
|
122
|
-
|
|
123
|
-
Point 3 is not a detail. `mapfile`/`readarray` are bash 4.0+, and where they are
|
|
124
|
-
missing — macOS ships bash 3.2, as do some minimal Linux images — the array is
|
|
125
|
-
simply never built, every field falls back to its default, and the desktop gets
|
|
126
|
-
a notification titled "LaCode" with an empty body. No error is printed anywhere
|
|
127
|
-
that anyone will read, because the receiver's stderr goes to whoever happened to
|
|
128
|
-
open the ssh connection. A `while IFS= read -r` loop is equivalent and works
|
|
129
|
-
everywhere; `bin/oc-notify-receiver` uses one.
|
|
130
|
-
|
|
131
|
-
### This half of the hop is Linux
|
|
132
|
-
|
|
133
|
-
The receiver delivers through `notify-send`, and the forward that reaches it is
|
|
134
|
-
a systemd `--user` unit (below). Both are Linux, so **the desktop half of this
|
|
135
|
-
tunnel does not exist on macOS**, and that is by design rather than a gap.
|
|
136
|
-
|
|
137
|
-
On a Mac you do not need it: `lacode notify` run locally picks `osascript` (or
|
|
138
|
-
`terminal-notifier`) and shows the notification itself, with no tunnel, no
|
|
139
|
-
`authorized_keys` entry and no systemd unit involved. The tunnel exists for the
|
|
140
|
-
case opencode runs *somewhere else*, and a Mac is not going to be running it.
|
|
141
|
-
|
|
142
|
-
If you point the receiver at a machine with no `notify-send` anyway, it exits
|
|
143
|
-
non-zero and says so, naming `lacode notify` as the path that does work there.
|
|
144
|
-
`bin/oc-sync` refuses the same machine for the same reason, and names each
|
|
145
|
-
missing prerequisite instead of failing somewhere less obvious.
|
|
146
|
-
|
|
147
|
-
> **Note.** The published package ships the built CLI (`dist/lacode.js`, linked
|
|
148
|
-
> as `lacode`) and the tunnel behind it (`dist/tunnel.js`, importable as
|
|
149
|
-
> `@lawaty/lacode/tunnel`), not a receiver script. The receiver is the
|
|
150
|
-
> desktop-side half and is yours to install — keep it out of your shell's
|
|
151
|
-
> `$PATH` reach and pinned in `authorized_keys`.
|
|
152
|
-
|
|
153
|
-
If the two halves disagree you get a blank popup, which points nowhere. A
|
|
154
|
-
receiver that sees a payload with no newline in it should say so on stderr
|
|
155
|
-
rather than silently defaulting everything.
|
|
156
|
-
|
|
157
|
-
## Holding the forward open
|
|
158
|
-
|
|
159
|
-
The tunnel has to exist whenever a notification is sent. Anything that survives
|
|
160
|
-
your logout works: a `--user` systemd unit is the usual choice, one per remote
|
|
161
|
-
host, each with its own port.
|
|
162
|
-
|
|
163
|
-
```ini
|
|
164
|
-
# ~/.config/systemd/user/oc-tunnel@<host>.service
|
|
165
|
-
[Service]
|
|
166
|
-
ExecStart=/usr/bin/ssh -NT -R 10022:localhost -o ClearAllForwardings=yes -o ExitOnForwardFailure=yes \
|
|
167
|
-
-i %h/.ssh/lacode_notify you@localhost
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Then `systemctl --user enable --now oc-tunnel@web-01`.
|
|
171
|
-
|
|
172
|
-
Two details that matter:
|
|
173
|
-
|
|
174
|
-
- **`-o ExitOnForwardFailure=yes`** — without it, ssh stays up with no forward
|
|
175
|
-
and the notifications fail silently.
|
|
176
|
-
- **`-o ClearAllForwardings=yes`** on every *other* ssh call that touches that
|
|
177
|
-
host, including rsync's `-e`. A `RemoteForward` in `~/.ssh/config` competes
|
|
178
|
-
for the same port, and a half-claimed listener starves the one session that
|
|
179
|
-
needed it.
|
|
127
|
+
## Legacy: the reverse forward
|
|
180
128
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
129
|
+
The original design opened a reverse forward (`ssh -R`) held by a `systemd --user`
|
|
130
|
+
unit on the desktop, and delivered to a `notify-send`-only bash receiver
|
|
131
|
+
(`bin/oc-notify-receiver`). It is kept only for hosts that still set
|
|
132
|
+
`OC_TUNNEL_PORT` and leave `OC_LOCAL_HOST` unset: `sendRemote` falls back to an
|
|
133
|
+
ssh call to `localhost:<port>`. New setups should use the direct ssh above. See
|
|
134
|
+
the git history for the old systemd unit shape.
|
package/docs/USER-STORIES.md
CHANGED
|
@@ -258,7 +258,7 @@ does not.
|
|
|
258
258
|
- [ ] Given a body containing `"`, `'`, `;`, `$(…)` or backticks, When the macOS send runs, Then it reaches `display notification` byte-for-byte as data and nothing is executed.
|
|
259
259
|
- [ ] Given a machine has no notifier for its platform, When a notification is sent, Then the result is a failed delivery with a reason and no exception, and `lacode doctor` names the channel that would run — or reports that there is none — instead of reporting a binary that is not used there.
|
|
260
260
|
- [ ] Given the CLI runs on macOS, When the send does not complete, Then it is still bounded by a timeout, exactly as on Linux.
|
|
261
|
-
- [ ] Given the
|
|
261
|
+
- [ ] Given the notification hop, When it is documented, Then the doc leads with the shipped path — the npm-installed plugin, a direct ssh hop (`ControlMaster`) set up by `lacode tunnel setup`, and the portable `lacode receive` forced command that delivers through the platform channel (`notify-send` or `osascript`) — and marks the older reverse-forward scripts (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`, `bin/oc-sync`) as the author's legacy tooling, so a Mac user is not sent to debug a script that was never meant to run on their machine.
|
|
262
262
|
- [ ] Given a machine without GNU `timeout` or with bash older than 4.0, When `bin/oc-sync` runs, Then it refuses **before** touching anything, names each missing prerequisite, and names the `lacode` subcommand to use instead. It must never report a misleading cause: the host list is read with `mapfile`, so without it the run used to die at "no hosts configured" — a confident wrong answer that sends the reader to edit `.env`.
|
|
263
263
|
- [ ] Given `bin/oc-notify-receiver` runs where `notify-send` does not exist, When it is invoked, Then it exits non-zero and names both the missing channel and the portable path, because its stderr reaches whoever opened the ssh connection and nobody else, and a bare "command not found" reads as a desktop that quietly stopped notifying.
|
|
264
264
|
- [ ] Given the payload is parsed, When the receiver splits `$SSH_ORIGINAL_COMMAND`, Then it splits on newlines only using constructs that exist in bash 3.2 as well as bash 5, and the suite **proves the split by running the receiver** rather than by finding the word `mapfile` in the file. Under bash 3.2 — the version macOS ships — a bash-4-only builtin left every field at its default, so the popup said "LaCode" with an empty body and no error anywhere: the same silent blank notification the argv bug caused once, reached again through a different door.
|
package/package.json
CHANGED