edge-ai-client-ts 1.0.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.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/bin/ec-ts.js +18 -0
- package/dist/buffer/disk-queue.d.ts +140 -0
- package/dist/buffer/disk-queue.js +370 -0
- package/dist/cli/devices.d.ts +1 -0
- package/dist/cli/devices.js +61 -0
- package/dist/cli/enroll.d.ts +2 -0
- package/dist/cli/enroll.js +89 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +116 -0
- package/dist/cli/messages.d.ts +1 -0
- package/dist/cli/messages.js +59 -0
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.js +112 -0
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +56 -0
- package/dist/cli/whoami.d.ts +2 -0
- package/dist/cli/whoami.js +41 -0
- package/dist/config/cmd-gate.d.ts +65 -0
- package/dist/config/cmd-gate.js +128 -0
- package/dist/config/settings.d.ts +209 -0
- package/dist/config/settings.js +627 -0
- package/dist/crypto/aes-gcm.d.ts +38 -0
- package/dist/crypto/aes-gcm.js +90 -0
- package/dist/crypto/hmac.d.ts +31 -0
- package/dist/crypto/hmac.js +52 -0
- package/dist/crypto/tls-guard.d.ts +36 -0
- package/dist/crypto/tls-guard.js +54 -0
- package/dist/daemon/manager.d.ts +82 -0
- package/dist/daemon/manager.js +461 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +63 -0
- package/dist/logging/file-logger.d.ts +21 -0
- package/dist/logging/file-logger.js +71 -0
- package/dist/network/ws-client.d.ts +221 -0
- package/dist/network/ws-client.js +1134 -0
- package/dist/session/fail-fast.d.ts +70 -0
- package/dist/session/fail-fast.js +122 -0
- package/dist/session/manager.d.ts +136 -0
- package/dist/session/manager.js +291 -0
- package/dist/session/persistence.d.ts +103 -0
- package/dist/session/persistence.js +194 -0
- package/dist/session/pi-rpc.d.ts +164 -0
- package/dist/session/pi-rpc.js +412 -0
- package/dist/session/sftp.d.ts +64 -0
- package/dist/session/sftp.js +335 -0
- package/dist/session/shell-frame.d.ts +77 -0
- package/dist/session/shell-frame.js +199 -0
- package/dist/session/shell.d.ts +124 -0
- package/dist/session/shell.js +300 -0
- package/docs/CONFIGURATION.md +169 -0
- package/docs/INSTALLATION.md +164 -0
- package/docs/PROTOCOL.md +248 -0
- package/docs/TROUBLESHOOTING.md +177 -0
- package/package.json +79 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `edge-ai-client-ts` are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/) and the
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- (placeholder for next iteration)
|
|
11
|
+
|
|
12
|
+
## [1.0.0] - 2026-07-13
|
|
13
|
+
|
|
14
|
+
**First public release** of `edge-ai-client-ts` on npm. The package
|
|
15
|
+
ships a complete, byte-compatible TypeScript re-implementation of the Rust
|
|
16
|
+
`ec` binary — same wire protocol, same security model, same CLI surface,
|
|
17
|
+
shipped via `npm install -g`.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Full TypeScript (Model 1) parallel client mirroring the Rust
|
|
21
|
+
`edge-client` byte-for-byte on the wire protocol. See
|
|
22
|
+
`docs/PROTOCOL.md` for the full spec and `e2e-smoke/README.md` for the
|
|
23
|
+
end-to-end smoke test.
|
|
24
|
+
- 17-byte binary shell frame codec (`session/shell-frame.ts`) — byte-exact
|
|
25
|
+
with Rust `ShellFrame::encode/decode`.
|
|
26
|
+
- AES-256-GCM disk-backed offline buffer queue (`buffer/disk-queue.ts`).
|
|
27
|
+
- AGT-02 fail-fast policy (`session/fail-fast.ts`).
|
|
28
|
+
- Disk-backed session persistence (`session/persistence.ts`).
|
|
29
|
+
- `EdgeWsClient` with register / heartbeat / ACK / `get_config` RPC /
|
|
30
|
+
reconnect supervisor.
|
|
31
|
+
- All six CLI subcommands (`run`, `enroll`, `whoami`, `status`, `devices`,
|
|
32
|
+
`messages`) with commander `exitOverride()` for embeddability.
|
|
33
|
+
- `ServiceManager` for per-OS service install: systemd --user (Linux),
|
|
34
|
+
LaunchAgent (macOS), schtasks / LeastPrivilege (Windows).
|
|
35
|
+
- TLS SPKI fingerprint pinning via the Node built-in `tls` module.
|
|
36
|
+
- Comprehensive documentation under `docs/`:
|
|
37
|
+
- `INSTALLATION.md`
|
|
38
|
+
- `CONFIGURATION.md`
|
|
39
|
+
- `TROUBLESHOOTING.md`
|
|
40
|
+
- `PROTOCOL.md`
|
|
41
|
+
- GitHub Actions workflow for CI + automated npm publish.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Subprotocol header is now a 2-element array `["edgeai-v2", "<token>"]`
|
|
45
|
+
(was a single comma-containing string that `ws@8` rejects).
|
|
46
|
+
- `socket.on('error')` defensive null-error handler (was propagating
|
|
47
|
+
`null` → `Error("null")`).
|
|
48
|
+
- `session/shell.ts` stub `FrameCodec` replaced with the byte-exact real
|
|
49
|
+
codec from `session/shell-frame.ts` (was producing DIFFERENT wire bytes
|
|
50
|
+
than the Rust side via a hex-string interpretation).
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- Renamed the package version series from the pre-publish `7.0.0` line to
|
|
54
|
+
`1.0.0` to mark the first public npm release (no API/semantic break —
|
|
55
|
+
internal only).
|
|
56
|
+
- Removed duplicate `FrameType` / `FrameCodec` stub from
|
|
57
|
+
`session/shell.ts` in favor of `session/shell-frame.ts` as the single
|
|
58
|
+
source of truth.
|
|
59
|
+
|
|
60
|
+
### Security
|
|
61
|
+
- `prepublishOnly` runs `typecheck`, `test`, and `build` to prevent
|
|
62
|
+
shipping a broken / unverified package.
|
|
63
|
+
- No secrets are stored in source. All credentials are loaded from
|
|
64
|
+
environment variables (`EDGE_AUTH_TOKEN`, `EDGE_BUFFER_KEY`,
|
|
65
|
+
`EDGE_RELAY_FINGERPRINT`, …) — see `docs/CONFIGURATION.md`.
|
|
66
|
+
|
|
67
|
+
### Known issues
|
|
68
|
+
- See `docs/TROUBLESHOOTING.md` for a curated list of operator-facing
|
|
69
|
+
workarounds (no known critical issues at v1.0.0).
|
|
70
|
+
|
|
71
|
+
[Unreleased]: https://github.com/baphuongna/edge-ai-agent/compare/v1.0.0...HEAD
|
|
72
|
+
[1.0.0]: https://github.com/baphuongna/edge-ai-agent/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edge AI Agent Contributors
|
|
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
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# edge-ai-client-ts
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/edge-ai-client-ts)
|
|
4
|
+
[](https://www.npmjs.com/package/edge-ai-client-ts)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
**TypeScript edge-client** for the [Edge AI Agent](https://github.com/baphuongna/edge-ai-agent) fleet — a pure-npm re-implementation of the Rust `ec` binary with a **byte-compatible wire protocol**.
|
|
10
|
+
|
|
11
|
+
A TypeScript edge-client and a Rust edge-client can connect to the **same relay** interchangeably, making `ec-ts` the perfect choice when:
|
|
12
|
+
|
|
13
|
+
- You want a one-line `npm install -g` install (no Rust toolchain required).
|
|
14
|
+
- You are running the edge node on an architecture without a prebuilt `ec` binary.
|
|
15
|
+
- You need to embed the client into a Node.js application or a custom daemon.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Install (requires Node.js >= 20)
|
|
23
|
+
npm install -g edge-ai-client-ts
|
|
24
|
+
|
|
25
|
+
# 2. Enroll against your relay (uses the per-operator API key from your dashboard)
|
|
26
|
+
ec-ts enroll --key <your-operator-api-key>
|
|
27
|
+
|
|
28
|
+
# 3. Start the daemon (foreground)
|
|
29
|
+
ec-ts run
|
|
30
|
+
|
|
31
|
+
# Or install it as a system service (auto-start on boot)
|
|
32
|
+
ec-ts run --install-service
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That's it — the daemon will open a WebSocket to the relay, register, and start sending heartbeats.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 📦 What you get
|
|
40
|
+
|
|
41
|
+
| Command | Purpose |
|
|
42
|
+
|---------|---------|
|
|
43
|
+
| `ec-ts run` | Start the daemon (foreground or `--install-service` for systemd/LaunchAgent/schtasks). |
|
|
44
|
+
| `ec-ts enroll --key <key>` | Persist a per-operator credential to the OS keychain. |
|
|
45
|
+
| `ec-ts whoami` | Print the current operator + relay identity. |
|
|
46
|
+
| `ec-ts status` | One-shot health snapshot (WS state, queue depth, last heartbeat). |
|
|
47
|
+
| `ec-ts devices` | List devices owned by the current operator. |
|
|
48
|
+
| `ec-ts messages` | Tail the message buffer. |
|
|
49
|
+
|
|
50
|
+
> **Binary name:** `ec-ts` (not `ec`) so you can run both the TypeScript and the Rust clients side-by-side if you want to compare them.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## ✅ Protocol parity with the Rust `ec`
|
|
55
|
+
|
|
56
|
+
The TypeScript client implements the **exact same wire formats** as the Rust client:
|
|
57
|
+
|
|
58
|
+
- **`/edge` JSON connection** — `register`, `stream`, `ack`, `heartbeat`, `get_config`, `list_agents`, `browser_fetch` message types with identical field names (`machine_id`, `agent_id`, `msg_id`, `type`, `payload`, `timestamp`).
|
|
59
|
+
- **`/shell-edge` binary frames** — 17-byte header = 16-byte `session_id` (UUID bytes) + 1-byte `FrameType` + payload. `FrameType` enum values are identical (`Data=0`, `Resize=1`, `Close=2`, …).
|
|
60
|
+
- **Buffer dump encryption** — `[KEY_VERSION=1][nonce:12][ciphertext+tag]` AES-256-GCM envelope with keyring rotation.
|
|
61
|
+
- **TLS SPKI pinning** — SHA-256 of the DER `SubjectPublicKeyInfo`.
|
|
62
|
+
- **`Sec-WebSocket-Protocol`** — `edgeai-v2, <token>[, device-secret=...]`.
|
|
63
|
+
|
|
64
|
+
A TS edge-client and a Rust edge-client produce **byte-compatible** frames and JSON.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📋 Requirements
|
|
69
|
+
|
|
70
|
+
- **Node.js >= 20** (uses `crypto.randomUUID`, native `fetch`, modern ESM).
|
|
71
|
+
- A C++ toolchain for `node-pty` — Microsoft `node-pty` ships **prebuilt `.node` binaries** for common platforms (Linux x64/arm64, macOS x64/arm64, Windows x64), so a toolchain is only needed on unusual architectures.
|
|
72
|
+
- The relay server must be reachable via WebSocket (`ws://` for dev, `wss://` for production).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## ⚙️ Configuration
|
|
77
|
+
|
|
78
|
+
Config lives at:
|
|
79
|
+
|
|
80
|
+
- **Linux:** `~/.config/edge-ai-agent/config.toml`
|
|
81
|
+
- **macOS:** `~/Library/Application Support/edge-ai-agent/config.toml`
|
|
82
|
+
- **Windows:** `%APPDATA%\edge-ai-agent\config.toml`
|
|
83
|
+
|
|
84
|
+
A JSON `config.json` is also accepted as a fallback. The schema is identical to the Rust `config.toml` — see [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for the full reference.
|
|
85
|
+
|
|
86
|
+
### Environment variables (preferred for secrets)
|
|
87
|
+
|
|
88
|
+
> **Never commit credentials to source.** Use environment variables for tokens, API keys, and encryption keys.
|
|
89
|
+
|
|
90
|
+
| Variable | Purpose |
|
|
91
|
+
|----------|---------|
|
|
92
|
+
| `EDGE_AUTH_TOKEN` | Fleet shared secret for WS auth (preferred over `config.toml`). |
|
|
93
|
+
| `EDGE_BUFFER_KEY` | Buffer-dump AES-256-GCM key (hex, 64 chars = 32 bytes). |
|
|
94
|
+
| `EDGE_BUFFER_KEY_PREVIOUS` | Previous key for rotation (hex, 64 chars). |
|
|
95
|
+
| `EDGE_RELAY_FINGERPRINT` | Pinned TLS SPKI fingerprint (overrides config). |
|
|
96
|
+
| `EDGE_OWNER_OPERATOR_ID` | Override the enrolled operator id. |
|
|
97
|
+
| `EDGEAI_REQUIRE_TLS` | `1` forces `wss://` even outside production. |
|
|
98
|
+
| `NODE_ENV=production` | Forces `wss://`. |
|
|
99
|
+
| `EDGE_CMD_GATE_MODE` | `off` / `warn` / `block`. |
|
|
100
|
+
| `EDGE_FAIL_FAST` | Fail-fast policy toggle. |
|
|
101
|
+
|
|
102
|
+
See [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for the full env-var reference and precedence rules.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🛠️ Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/baphuongna/edge-ai-agent.git
|
|
110
|
+
cd edge-ai-agent/edge-client-ts
|
|
111
|
+
npm install
|
|
112
|
+
npm run typecheck # tsc --noEmit
|
|
113
|
+
npm test # vitest run
|
|
114
|
+
npm run build # compile to dist/
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pre-publish sanity check (mirrors CI):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run prepublishOnly # typecheck + test + build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
End-to-end smoke test against a live `ws-service`:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm run smoke
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) for common install/run issues.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🧱 Architecture (module mapping)
|
|
134
|
+
|
|
135
|
+
Every module in this package corresponds 1:1 with a Rust module — see
|
|
136
|
+
[`docs/PROTOCOL.md`](docs/PROTOCOL.md) for the byte-exact wire spec.
|
|
137
|
+
|
|
138
|
+
| Rust file | TS file |
|
|
139
|
+
|------------------------------------|----------------------------------|
|
|
140
|
+
| `config/settings.rs` | `src/config/settings.ts` |
|
|
141
|
+
| `config/machine_id.rs` | `src/config/settings.ts` |
|
|
142
|
+
| `config/device_secret.rs` | `src/config/settings.ts` |
|
|
143
|
+
| `session/cmd_gate.rs` (config) | `src/config/cmd-gate.ts` |
|
|
144
|
+
| `network/tls_guard.rs` | `src/crypto/tls-guard.ts` |
|
|
145
|
+
| `crypto/aes_gcm.rs` | `src/crypto/aes-gcm.ts` |
|
|
146
|
+
| `logging/file_logger.rs` | `src/logging/file-logger.ts` |
|
|
147
|
+
| `network/ws_client.rs` | `src/network/ws-client.ts` |
|
|
148
|
+
| `session/manager.rs` | `src/session/manager.ts` |
|
|
149
|
+
| `session/shell.rs` | `src/session/shell.ts` |
|
|
150
|
+
| `session/shell_frame.rs` | `src/session/shell-frame.ts` |
|
|
151
|
+
| `session/fail_fast.rs` | `src/session/fail-fast.ts` |
|
|
152
|
+
| `session/pi_rpc.rs` | `src/session/pi-rpc.ts` |
|
|
153
|
+
| `session/persistence.rs` | `src/session/persistence.ts` |
|
|
154
|
+
| `session/sftp.rs` | `src/session/sftp.ts` |
|
|
155
|
+
| `buffer/disk_queue.rs` | `src/buffer/disk-queue.ts` |
|
|
156
|
+
| `cli/mod.rs` | `src/cli/index.ts` |
|
|
157
|
+
| `daemon/manager.rs` | `src/daemon/manager.ts` |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📚 Documentation
|
|
162
|
+
|
|
163
|
+
- [`docs/INSTALLATION.md`](docs/INSTALLATION.md) — Detailed install guide (Linux / macOS / Windows, dev / prod).
|
|
164
|
+
- [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) — Full config schema + env-var precedence.
|
|
165
|
+
- [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — Common errors and fixes.
|
|
166
|
+
- [`docs/PROTOCOL.md`](docs/PROTOCOL.md) — Wire-protocol reference (frames, JSON messages, TLS pinning).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 📄 License
|
|
171
|
+
|
|
172
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
173
|
+
|
|
174
|
+
This package is part of the Edge AI Agent monorepo. The full system is documented at <https://github.com/baphuongna/edge-ai-agent>.
|
package/bin/ec-ts.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ec-ts — TypeScript (Model 1) parallel edge-client binary shim.
|
|
4
|
+
*
|
|
5
|
+
* Loader that forwards argv to the CLI dispatch in the compiled dist/index.js.
|
|
6
|
+
* Install globally via `npm i -g edge-ai-client-ts` and run `ec-ts`.
|
|
7
|
+
*
|
|
8
|
+
* Requires Node.js >= 20.
|
|
9
|
+
*/
|
|
10
|
+
import('../dist/cli/index.js')
|
|
11
|
+
.then(async ({ dispatch }) => {
|
|
12
|
+
const code = await dispatch(process.argv.slice(2));
|
|
13
|
+
process.exit(code);
|
|
14
|
+
})
|
|
15
|
+
.catch((err) => {
|
|
16
|
+
console.error('ec-ts: failed to load CLI dispatch:', err);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk-backed offline buffer queue — TypeScript parallel of
|
|
3
|
+
* `edge-client/src/buffer/queue.rs` (in-memory queue with the disk-persist
|
|
4
|
+
* half) and `edge-client/src/buffer/disk_dump.rs` (AES-256-GCM envelope).
|
|
5
|
+
*
|
|
6
|
+
* This is the OFFLINE RESILIENCE LAYER: when the WS connection is down,
|
|
7
|
+
* stream frames are durably written to disk; on reconnect they replay in
|
|
8
|
+
* order; no frames lost on crash.
|
|
9
|
+
*
|
|
10
|
+
* ## On-disk format (mirrors Rust `DiskDumper`)
|
|
11
|
+
*
|
|
12
|
+
* [KEY_VERSION: 1 byte][AES-256-GCM nonce: 12 bytes][ciphertext + auth tag: 16 bytes]
|
|
13
|
+
*
|
|
14
|
+
* The plaintext is a JSON-encoded array of {@link QueueEntry} objects
|
|
15
|
+
* (`{ msg_id, timestamp, bytes }`), where `bytes` is base64-encoded.
|
|
16
|
+
*
|
|
17
|
+
* ## Atomic-write invariant (the project's hardest-won guarantee)
|
|
18
|
+
*
|
|
19
|
+
* 1. Serialize entries → encrypt → write to `<dir>/.buffer.queue.enc.tmp`
|
|
20
|
+
* 2. `fsync` the temp file
|
|
21
|
+
* 3. `rename` tmp → `<dir>/buffer.queue.enc`
|
|
22
|
+
*
|
|
23
|
+
* Crash safety: a tmp file left on disk after a crash is half-written; we
|
|
24
|
+
* delete it on next lazy-load. The main file is either fully written or
|
|
25
|
+
* absent (the rename is atomic on POSIX and on Windows since Node 14+).
|
|
26
|
+
*
|
|
27
|
+
* ## Concurrency
|
|
28
|
+
*
|
|
29
|
+
* In-process serialization via a Promise-chain mutex. Cross-process locking
|
|
30
|
+
* is intentionally NOT implemented (would require `proper-lockfile` or
|
|
31
|
+
* similar; omitted to keep the dependency surface minimal — the in-process
|
|
32
|
+
* case is the common path).
|
|
33
|
+
*
|
|
34
|
+
* ## Dedup
|
|
35
|
+
*
|
|
36
|
+
* `append` silently skips when an entry with the same `msg_id` is already
|
|
37
|
+
* buffered. The Rust `BufferQueue::push` does not dedup, but the disk queue
|
|
38
|
+
* MUST dedup to avoid sending the same `msg_id` twice to the relay on
|
|
39
|
+
* reconnect (relay-side ACKs would otherwise race).
|
|
40
|
+
*/
|
|
41
|
+
import { KEY_VERSION } from '../crypto/aes-gcm.js';
|
|
42
|
+
/** One queued message awaiting replay. */
|
|
43
|
+
export interface QueueEntry {
|
|
44
|
+
/** Stable identifier used for dedup. */
|
|
45
|
+
msg_id: string;
|
|
46
|
+
/** Unix epoch milliseconds (sender-side ordering hint). */
|
|
47
|
+
timestamp: number;
|
|
48
|
+
/** Wire payload — encrypted at rest in the on-disk file. */
|
|
49
|
+
bytes: Uint8Array;
|
|
50
|
+
}
|
|
51
|
+
export interface DiskBufferQueueOptions {
|
|
52
|
+
/** Directory where the encrypted queue file lives. Created if missing. */
|
|
53
|
+
readonly dir: string;
|
|
54
|
+
/** 32-byte AES-256-GCM key (typically from `loadBufferKey(settings)`). */
|
|
55
|
+
readonly key: Uint8Array;
|
|
56
|
+
/**
|
|
57
|
+
* Soft cap on the serialized JSON envelope (plaintext bytes).
|
|
58
|
+
* Mirrors Rust `BufferConfig.max_memory_mb` (default 50 MiB).
|
|
59
|
+
*/
|
|
60
|
+
readonly maxBytes?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Default cap — 50 MiB. Matches Rust `BufferConfig::default().max_memory_mb`. */
|
|
63
|
+
export declare const DEFAULT_MAX_BYTES: number;
|
|
64
|
+
/** Filename of the encrypted queue blob (relative to `dir`). */
|
|
65
|
+
export declare const QUEUE_FILE_NAME = "buffer.queue.enc";
|
|
66
|
+
/** Filename of the atomic-write temp file (relative to `dir`). */
|
|
67
|
+
export declare const QUEUE_TMP_NAME = ".buffer.queue.enc.tmp";
|
|
68
|
+
/** Thrown by {@link DiskBufferQueue.append} when the cap would be exceeded. */
|
|
69
|
+
export declare class BufferFullError extends Error {
|
|
70
|
+
readonly currentBytes: number;
|
|
71
|
+
readonly maxBytes: number;
|
|
72
|
+
constructor(currentBytes: number, maxBytes: number);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Single-file, AES-256-GCM-encrypted, FIFO buffer queue.
|
|
76
|
+
*
|
|
77
|
+
* Append-only on the hot path. `drain()` consumes every entry in FIFO order,
|
|
78
|
+
* hands each to a handler, and deletes the on-disk file when the handler
|
|
79
|
+
* completes successfully. `peek()` and `size()` are non-mutating.
|
|
80
|
+
*/
|
|
81
|
+
export declare class DiskBufferQueue {
|
|
82
|
+
private readonly dir;
|
|
83
|
+
private readonly filePath;
|
|
84
|
+
private readonly tmpPath;
|
|
85
|
+
private readonly key;
|
|
86
|
+
private readonly maxBytes;
|
|
87
|
+
/** In-memory cache. Loaded lazily on first op. */
|
|
88
|
+
private entries;
|
|
89
|
+
private loaded;
|
|
90
|
+
/** Promise-chain mutex for in-process serialization. */
|
|
91
|
+
private mutex;
|
|
92
|
+
constructor(opts: DiskBufferQueueOptions);
|
|
93
|
+
/**
|
|
94
|
+
* Append an entry to the queue. Skips silently when `entry.msg_id` is
|
|
95
|
+
* already buffered (dedup invariant). Throws {@link BufferFullError} when
|
|
96
|
+
* the serialized envelope would exceed `maxBytes`.
|
|
97
|
+
*/
|
|
98
|
+
append(entry: QueueEntry): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Return up to `limit` entries (FIFO order) without removing them.
|
|
101
|
+
* Defensive copy — callers may freely mutate the returned bytes.
|
|
102
|
+
*/
|
|
103
|
+
peek(limit: number): Promise<QueueEntry[]>;
|
|
104
|
+
/** Return the number of entries currently buffered. */
|
|
105
|
+
size(): Promise<number>;
|
|
106
|
+
/**
|
|
107
|
+
* Delete all entries and the on-disk file. Idempotent — succeeds whether
|
|
108
|
+
* the file exists or not.
|
|
109
|
+
*/
|
|
110
|
+
clear(): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Invoke `handler(entry)` for every entry in FIFO order, then delete the
|
|
113
|
+
* on-disk file. Returns the number of entries processed. If the handler
|
|
114
|
+
* throws, the entries remain on disk for a future `drain()` retry
|
|
115
|
+
* (at-least-once semantics — matches the Rust offline-queue contract).
|
|
116
|
+
*/
|
|
117
|
+
drain(handler: (entry: QueueEntry) => Promise<void> | void): Promise<number>;
|
|
118
|
+
/**
|
|
119
|
+
* Lazily create the dir, recover from a prior crash (delete tmp), and
|
|
120
|
+
* load existing entries. Runs at most once per queue instance.
|
|
121
|
+
*/
|
|
122
|
+
private ensureLoaded;
|
|
123
|
+
/**
|
|
124
|
+
* Encrypt + atomic-write the in-memory entry list to disk.
|
|
125
|
+
*
|
|
126
|
+
* Sequence (mirrors Rust `atomic_write` in `disk_dump.rs:159-186`):
|
|
127
|
+
* 1. encrypt(plaintext) → `[version][nonce][ciphertext][tag]`
|
|
128
|
+
* 2. open tmp with mode 0600 (defense in depth — encryption protects
|
|
129
|
+
* contents either way)
|
|
130
|
+
* 3. write blob + fsync the file (force kernel buffers to disk)
|
|
131
|
+
* 4. rename tmp → final path (atomic on POSIX, atomic on Win since Node 14+)
|
|
132
|
+
*/
|
|
133
|
+
private persist;
|
|
134
|
+
private projectedBytesAfterAppend;
|
|
135
|
+
/** Serialize operations via a Promise chain (in-process mutex). */
|
|
136
|
+
private withLock;
|
|
137
|
+
private assertValidEntry;
|
|
138
|
+
private unlinkIfExists;
|
|
139
|
+
}
|
|
140
|
+
export { KEY_VERSION };
|