@domsync/win32-arm64 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 neiromaster
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,301 @@
1
- # Temporary Holding Version
1
+ # domsync
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
+ `domsync` keeps a single domain list in sync between your laptop and an OpenClash rule-provider file on a router. It reads the local list and the provider file over SSH, merges them (the local entries win, router-only domains go into an `other` section), validates everything, then atomically rewrites each file that changed and optionally restarts OpenClash.
4
+
5
+ It never silently rewrites anything it cannot parse: an ambiguous local file or a malformed provider line stops the run before a single byte is written.
6
+
7
+ **Not supported in v1:** homeproxy, subscription URLs or any remote source other than the current provider file, SSH password authentication, `ProxyJump`, a fallback to the system `ssh`, IDN domains (use punycode), and cryptographic verification of updates inside the CLI. There is no daemon, watcher or scheduler; run `domsync` from cron or by hand.
8
+
9
+ ## Install
10
+
11
+ Homebrew (cask tap):
12
+
13
+ ```sh
14
+ brew install --cask neiromaster/tap/domsync
15
+ ```
16
+
17
+ The cask works on macOS and, with Homebrew 6.0 or newer, on Linux.
18
+
19
+ npm (macOS, Linux and Windows):
20
+
21
+ ```sh
22
+ npm i -g @domsync/cli
23
+ ```
24
+
25
+ Install script (macOS and Linux):
26
+
27
+ ```sh
28
+ curl -fsSL https://github.com/neiromaster/domsync/releases/latest/download/install.sh | sh
29
+ ```
30
+
31
+ The script downloads the release archive and `checksums.txt`, verifies the SHA-256 checksum, and installs to `$DOMSYNC_INSTALL_DIR` (default `$HOME/.local/bin`). Set `DOMSYNC_VERSION` (for example `1.2.3` or `v1.2.3`) to pin a release instead of the latest one. It never uses `sudo`; if the destination is not writable it prints instructions instead. If `cosign` is installed it also verifies the Sigstore bundle, otherwise it prints the manual verification commands.
32
+
33
+ ## Quick start
34
+
35
+ ```sh
36
+ cp domsync.example.yaml domsync.yaml
37
+ cat > domains.txt <<'EOF'
38
+ [media]
39
+ +.example.com
40
+ +.example.net
41
+
42
+ [misc]
43
+ +.example.org
44
+ EOF
45
+ ```
46
+
47
+ Edit `domsync.yaml` for your router (`target.host`, `target.user`, `target.ssh_key`, `target.path`), make sure the router's host key is already in `~/.ssh/known_hosts`, then run:
48
+
49
+ ```sh
50
+ domsync
51
+ ```
52
+
53
+ The config is looked up in this order: `--config <path>`, the `DOMSYNC_CONFIG` environment variable, then `./domsync.yaml` in the current directory. Relative paths inside the config (`source.domains`, `target.ssh_key`, `target.known_hosts`, `logging.file`) are resolved against the config file's directory; `~` and `~/...` are expanded to your home directory.
54
+
55
+ A first run needs the router's host key in `known_hosts`. If it is missing, the error includes the SHA-256 fingerprint and the exact `ssh-keyscan -H ... >> ~/.ssh/known_hosts` command to add it.
56
+
57
+ ## OpenClash provider
58
+
59
+ Add this provider to the OpenClash configuration:
60
+
61
+ ```yaml
62
+ rule-providers:
63
+ domsync:
64
+ type: file
65
+ behavior: domain
66
+ format: text
67
+ path: ./rule_provider/domsync.txt
68
+ ```
69
+
70
+ `format: text` is **mandatory**. Without it Mihomo parses the file as YAML, the provider ends up empty, and OpenClash gives no error.
71
+
72
+ `target.path` in `domsync.yaml` is the absolute path of the same file; OpenClash keeps provider files under `/etc/openclash/rule_provider/`.
73
+
74
+ `/etc/init.d/openclash reload` does **not** re-read provider files. Use `restart`, which is why the example config's post-write command is `/etc/init.d/openclash restart`. Any other command that genuinely reloads providers works too.
75
+
76
+ In the provider file, `# [section]` lines are comments for humans only: Mihomo ignores lines starting with `#` or `//`. domsync writes section headers only in that commented form, because a bare `[media]` line would be accepted by Mihomo as a literal domain and silently inflate the rule count. Comments are never preserved from the router: the file is regenerated from the local state.
77
+
78
+ ## Local file format
79
+
80
+ ```text
81
+ # A note about the whole file
82
+
83
+ [media]
84
+ +.bitru.org
85
+ +.nnmclub.to
86
+ -+.servarr.com
87
+
88
+ [misc]
89
+ +.example.org
90
+ +.servarr.com
91
+ ```
92
+
93
+ ### Sections
94
+
95
+ A section header is `[name]` on its own line, optionally with surrounding whitespace. Names must be non-empty, at most 64 characters, free of control characters, and unique. Section order follows first appearance in the local file, but the `target.other_section` section (default `other`) is always rendered last. Empty sections are dropped, together with their comments.
96
+
97
+ A file with no section headers is a flat list and stays flat; in that case no `other` section is ever created, even if router-only domains are adopted.
98
+
99
+ ### Patterns
100
+
101
+ Every positive entry is a Mihomo domain pattern:
102
+
103
+ | Form | Meaning |
104
+ |---|---|
105
+ | `example.com` | that exact domain |
106
+ | `.example.com` | subdomains only |
107
+ | `*.example.com` | exactly one extra label |
108
+ | `+.example.com` | the domain and all subdomains |
109
+ | `*.*.example.com` | wildcards on two labels |
110
+
111
+ Rules enforced by the parser:
112
+
113
+ - `+` may appear only as the whole first label and only when at least one more label follows; `*` must occupy a whole label.
114
+ - Labels may contain ASCII letters, digits, `_` and `-`; a hyphen may not be the first or last character of a label.
115
+ - A slash is forbidden, a trailing dot is rejected, and non-ASCII is rejected with a punycode hint.
116
+ - Entries must start at column 0: leading or trailing whitespace is an error.
117
+ - The pattern is lowercased and otherwise kept verbatim. `example.com`, `.example.com`, `*.example.com` and `+.example.com` are different rules and domsync never canonicalizes one into another.
118
+
119
+ ### Deletion and movement
120
+
121
+ A leading `-` is the only structural marker: `-` followed by any valid pattern is a deletion directive. A `+` or `*` inside the pattern is Mihomo syntax, not a sign.
122
+
123
+ - `-pattern` with no positive entry for the same pattern **deletes** it from the router and from the local snapshot.
124
+ - `-pattern` plus a positive entry for the same pattern in a **different** section **moves** it to that section. On the router only the section comment changes.
125
+ - `-pattern` and a positive entry in the **same** section is redundant: the positive entry wins and a warning is printed. With `--strict` it is an error.
126
+ - A `-pattern` that exists neither locally nor on the router produces a warning; `--strict` turns it into an error.
127
+
128
+ To delete a domain that still has a positive line, remove that positive line and add `-pattern`. While the positive line remains, domsync treats the pair as a move, not a deletion.
129
+
130
+ After a successful sync the `-` lines disappear from the local file: the domain has either been removed or is now written in its destination section. Duplicate positive patterns and duplicate `-` directives are errors.
131
+
132
+ ### Comments
133
+
134
+ - `#` at the start of a line is a comment. Comments are local-only: they are kept in the local snapshot and are never written to the router.
135
+ - Comments attach to the current section and are rendered immediately after that section's header, in their original order. Comments before the first section stay at the top of the file.
136
+ - Inline comments are invalid. The line `+.example.com # note` is a validation error; put the note on its own line.
137
+ - `//` is not a local comment. A local line starting with `//` is a pattern and is rejected.
138
+ - Blank lines are ignored, a UTF-8 BOM is stripped, and CRLF is accepted.
139
+
140
+ ### Output rules
141
+
142
+ Within each section, patterns are sorted; section order is otherwise preserved; the `other_section` is last. The local file is rewritten as a snapshot of the merged state, comments included. When the local file has sections, the router file contains `# [section]` headers followed by the same sorted patterns; a flat local file produces a flat router file. Router-only domains are added to `other_section`; if the local file already has that section it is reused, so a second one is never created.
143
+
144
+ A missing local file is treated as an empty list, so the first run bootstraps the local snapshot from the router: every router domain is adopted and written locally. An empty local file with an empty router is a no-op. A plan whose final list would be empty while the router is not is an error: domsync refuses to wipe the router.
145
+
146
+ ### Router file validation
147
+
148
+ When reading the provider file from the router, domsync ignores blank lines and lines starting with `#` or `//`. Every other line must be a valid pattern; otherwise the run fails with the file, line number and offending text, and nothing is written. Duplicate patterns are collapsed. A bare `[media]` header on the router is therefore an error, not a domain.
149
+
150
+ ## Configuration reference
151
+
152
+ YAML parsing is strict: unknown fields, multiple YAML documents and an empty file are errors.
153
+
154
+ | Key | Type | Default | Meaning |
155
+ |---|---|---|---|
156
+ | `version` | integer | required | Must be `1`. |
157
+ | `source.domains` | path | `domains.txt` | Local domain list, resolved relative to the config file. |
158
+ | `target.host` | string | required | Router host name or address. |
159
+ | `target.port` | integer | `22` | SSH port (1-65535). |
160
+ | `target.user` | string | required | SSH user. |
161
+ | `target.ssh_key` | path | empty | Private key (ed25519, ecdsa or rsa). If empty, `SSH_AUTH_SOCK` must provide an agent. Both are used when available. |
162
+ | `target.known_hosts` | path | `~/.ssh/known_hosts` | Must already exist and be a regular file. |
163
+ | `target.path` | string | required | Absolute path of the provider file on the router; a trailing `/` is rejected. |
164
+ | `target.other_section` | string | `other` | Section that receives router-only domains. |
165
+ | `target.mode` | string | `0644` | Octal permissions for the written provider file, at most `0777`. Quote it in YAML. |
166
+ | `target.backup` | boolean | `true` | Copy the previous provider file to the backup path before replacing it. |
167
+ | `target.backup_suffix` | string | `.domsync.bak` | Suffix appended to `target.path` for the backup file. |
168
+ | `target.post_write.run` | string | `on_change` | When to run `target.post_write.commands`: `on_change`, `always` or `never`. |
169
+ | `target.post_write.timeout` | duration string | `120s` | Timeout for each command. See the note below. |
170
+ | `target.post_write.commands` | list of strings | empty | Commands run sequentially over the existing SSH connection. |
171
+ | `update.check` | boolean | `true` | Check GitHub for a newer release at launch. `--no-update-check` overrides it. |
172
+ | `logging.file` | path | empty (disabled) | When set, one summary line is appended on a completed run (and when a post-write command fails). |
173
+
174
+ `target.post_write.run` values:
175
+
176
+ - `on_change` - run the commands only when the router provider file changed.
177
+ - `always` - run them after every successful sync, including a run with no changes at all.
178
+ - `never` - never run them.
179
+
180
+ `--dry-run` never runs commands, and no commands run when an earlier step failed. A failing command stops the sequence and exits with code 5.
181
+
182
+ **The command timeout cannot be disabled.** `timeout` applies to each command separately. A value of `0s` is treated as "unset" and replaced by the built-in default of `120s`; negative values are rejected. Choose a large finite value if a command legitimately runs long.
183
+
184
+ **Host keys are always verified.** This is true for key-file authentication and for ssh-agent authentication alike. There is no trust-on-first-use mode.
185
+
186
+ Config validation runs before SSH is dialled: a missing key file, a missing `known_hosts`, or an empty `target.ssh_key` with `SSH_AUTH_SOCK` unset fails with exit code 1 before any connection is attempted.
187
+
188
+ ## Exit codes
189
+
190
+ | Code | Meaning |
191
+ |---|---|
192
+ | `0` | Success, including "no changes". Also `--help` and `--version`. |
193
+ | `1` | Usage or configuration error, local file read/write error, or log write error. |
194
+ | `2` | Validation error in the local list or the router provider file, including `--strict` warnings, and an empty final list. |
195
+ | `3` | SSH connection, authentication, host-key or remote read/write/verify error. |
196
+ | `4` | Update error, including the Homebrew/npm refusal. |
197
+ | `5` | A post-write command failed (non-zero exit or transport error). |
198
+
199
+ `--strict` only promotes local warnings (deletion target not found, redundant `+`/`-` in one section) to errors. Problems in the router file are always errors, and a failed launch-time update check never changes the sync exit code.
200
+
201
+ ## Updates
202
+
203
+ On every ordinary run, if `update.check` is `true` and `--no-update-check` was not passed, domsync asks the GitHub releases API for the latest tag with a 2-second budget and an ETag cache under `os.UserCacheDir()/domsync/update.json`. When a newer version exists it prints one line to stderr after the report:
204
+
205
+ ```text
206
+ domsync: доступна v1.4.0 (у вас v1.3.2). Обновление: domsync update
207
+ ```
208
+
209
+ The notice is suppressed by `--quiet` and `--json`; in JSON the report carries an `update` object with `current`, `latest` and `available`. A network or API failure is silent, or a warning in `--verbose`; it never fails the sync.
210
+
211
+ Explicit update commands:
212
+
213
+ ```sh
214
+ domsync update # download, verify SHA-256 and replace the binary
215
+ domsync update --check # check and report only; changes nothing
216
+ ```
217
+
218
+ `domsync update --check` prints the current version and either the available version or `Обновлений нет`. `update.check: false` does not affect these explicit commands.
219
+
220
+ domsync does not replace binaries installed by Homebrew or npm. The install method is detected from the resolved executable path, and for those installs it prints this and exits with code 4 (the message itself is Russian):
221
+
222
+ ```text
223
+ domsync установлен через Homebrew. Обновление: brew upgrade --cask domsync
224
+ domsync установлен через npm. Обновление: npm i -g @domsync/cli@latest
225
+ ```
226
+
227
+ So update those installs with `brew upgrade --cask domsync` or `npm i -g @domsync/cli@latest`. The same refusal applies to `domsync update --check`, because install detection happens first. A standalone binary is replaced atomically after its archive has been checked against `checksums.txt`.
228
+
229
+ ## Output
230
+
231
+ The default human report is Russian and looks like this for a run that changed both files:
232
+
233
+ ```text
234
+ domsync: 14 доменов, 3 секции
235
+ роутер: +1 −1, записан, бэкап /etc/openclash/rule_provider/domsync.txt.domsync.bak
236
+ локально: +1 −1, перемещён 1
237
+ команда: /etc/init.d/openclash restart — ok, 2.1s
238
+ ```
239
+
240
+ - The header reports the total number of domains and non-empty sections.
241
+ - `роутер` and `локально` show added and removed counts with the U+2212 minus sign. The router line adds `записан` only when the file was actually written, and the backup path when a backup was made; the local line adds `перемещён N` for moved patterns.
242
+ - `без изменений` means that file was already identical.
243
+ - With `--dry-run`, each changed file is marked `будет записан (dry-run)`.
244
+ - Command results are `ok, <duration>` or `ошибка <exit code>`; a transport failure prints `ошибка: <message>`.
245
+ - Warnings print as `предупреждение: <message>`.
246
+
247
+ `--dry-run` still connects to the router and reads the current provider file; it skips both writes and all commands. It is not an offline mode.
248
+
249
+ - `--json` writes a single JSON object to stdout; errors after argument parsing are JSON as well. The English field names are `ok`, `changed`, `dry_run`, `total`, `sections`, `router`, `local`, `warnings`, `commands`, `update`, `error`.
250
+ - `--quiet` prints only errors; `--verbose` adds update-check failures to the warnings; `--strict` treats local warnings as errors.
251
+
252
+ ## Security
253
+
254
+ - **Strict host key checking.** Every connection verifies the router key against `known_hosts` using `golang.org/x/crypto/ssh/knownhosts`, whether authentication uses a key file or the ssh-agent. An unknown key aborts with its SHA-256 fingerprint and a `ssh-keyscan` hint; a changed key aborts naming both the presented and the expected fingerprints. TOFU does not exist in v1. The host key type is negotiated from the algorithms recorded for the host in `known_hosts`, so a host with only one recorded key type is not rejected merely because the server also offers another.
255
+ - **Updates in v1 rely on SHA-256 only.** The binary checks the downloaded archive against `checksums.txt` from the release and extracts only the expected executable, rejecting path traversal. GoReleaser also publishes a cosign keyless signature bundle (`checksums.txt.sigstore.json`) and GitHub build attestations; these are for manual verification, not checked by the CLI. To verify a release by hand:
256
+
257
+ ```sh
258
+ curl -fsSL -O https://github.com/neiromaster/domsync/releases/latest/download/checksums.txt
259
+ curl -fsSL -O https://github.com/neiromaster/domsync/releases/latest/download/checksums.txt.sigstore.json
260
+ cosign verify-blob --bundle checksums.txt.sigstore.json \
261
+ --certificate-identity-regexp '^https://github.com/neiromaster/domsync/' \
262
+ --certificate-oidc-issuer-regexp '^https://token\.actions\.githubusercontent\.com$' \
263
+ checksums.txt
264
+ ```
265
+
266
+ - **`install.sh` verifies SHA-256 before installing** and refuses to continue if neither `sha256sum` nor `shasum` is available. It also verifies the cosign bundle when `cosign` is installed.
267
+ - **Remote writes are atomic and verified.** Contents go over stdin, not the command line. A temporary file is renamed over the target, the previous file is optionally copied to the backup path, symlinks are resolved so the link keeps pointing at the updated file, and the result is read back and compared byte for byte. A directory target is an error.
268
+ - SSH password authentication, `ProxyJump` and the system `ssh` binary are not used.
269
+
270
+ ## Development
271
+
272
+ ```sh
273
+ go test ./...
274
+ ./scripts/check-coverage.sh
275
+ go tool golangci-lint run
276
+ go tool govulncheck ./...
277
+ ```
278
+
279
+ Fuzzing:
280
+
281
+ ```sh
282
+ go test ./internal/domainlist -run '^$' -fuzz FuzzParseLocal -fuzztime 30s
283
+ go test ./internal/domainlist -run '^$' -fuzz FuzzParseRemote -fuzztime 30s
284
+ ```
285
+
286
+ `lefthook install` enables the pre-commit hook (gofmt, golangci-lint, tests).
287
+
288
+ ## Release
289
+
290
+ Tagging `v*` runs `.github/workflows/release.yml`. Maintainers need:
291
+
292
+ - **No npm token.** The `npm` job requests `id-token: write` and `.github/scripts/publish-npm.sh` publishes the `@domsync/cli` launcher and the six `@domsync/<os>-<cpu>` platform packages through [trusted publishing](https://docs.npmjs.com/trusted-publishers/): npm exchanges the GitHub OIDC token for a short-lived credential and attaches provenance. Each of the seven package names needs a trusted publisher configured on npmjs.com (`neiromaster` / `domsync` / `release.yml`, no environment), or on the command line with `npm trust github <package> --file release.yml --repo neiromaster/domsync --allow-publish`.
293
+ npm only accepts a trusted publisher for a package that already exists, so the first release is published once by a maintainer: run `npm login` (2FA), then, after the GitHub release exists, `GITHUB_TOKEN=$(gh auth token) GITHUB_REF_NAME=<tag> GITHUB_REPOSITORY=neiromaster/domsync ./.github/scripts/publish-npm.sh`. After that, tag pushes publish without credentials. The script generates provenance only inside GitHub Actions, so the bootstrap version is published without it.
294
+ - A `neiromaster/homebrew-tap` repository and a `HOMEBREW_TAP_TOKEN` secret with write access to it, used by the cask publisher.
295
+ - A **public** repository. The Scorecard workflow sets `publish_results: true`, which requires public visibility.
296
+
297
+ GoReleaser builds the archives, `checksums.txt`, the cosign bundle and SBOMs, publishes the GitHub release and the Homebrew cask; the release workflow adds build provenance attestations and then publishes the npm packages.
298
+
299
+ ## License
300
+
301
+ MIT
package/domsync.exe ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1 @@
1
- {
2
- "name": "@domsync/win32-arm64",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {"name":"@domsync/win32-arm64","version":"1.0.1","os":"win32","cpu":"arm64","files":["domsync","domsync.exe"],"license":"MIT","repository":{"type":"git","url":"git+https://github.com/neiromaster/domsync.git"},"publishConfig":{"provenance":true,"access":"public"}}