appduct 0.11.0 → 0.12.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/README.md +4 -4
- package/dist/bin.js +7 -1
- package/dist/cli/result-types.d.ts +5 -4
- package/dist/cli/routes/daemon/run.js +83 -21
- package/dist/cli/routes/daemon/start.js +18 -3
- package/dist/cli/routes/daemon/status.js +18 -3
- package/dist/cli/routes/daemon/stop.js +18 -3
- package/dist/cli/routes/events.js +20 -4
- package/dist/cli/routes/invoke.js +18 -3
- package/dist/cli/routes/keygen.js +1 -0
- package/dist/cli/routes/link.js +18 -3
- package/dist/cli/routes/ls.js +18 -3
- package/dist/cli/routes/mcp.js +33 -19
- package/dist/cli/routes/revoke.js +18 -3
- package/dist/cli/routes/tools.js +18 -3
- package/dist/client/app-client.d.ts +4 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +22 -8
- package/dist/commands/events.d.ts +3 -2
- package/dist/daemon/config.d.ts +4 -1
- package/dist/daemon/daemon.d.ts +3 -0
- package/dist/daemon/event-bus.d.ts +4 -4
- package/dist/daemon/events-log.d.ts +31 -0
- package/dist/daemon/memory-append-only-file.d.ts +17 -0
- package/dist/daemon/node-append-only-file.d.ts +9 -0
- package/dist/daemon/state-dir.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/mcp/app-tools.d.ts +7 -7
- package/dist/mcp/events-tool.d.ts +0 -7
- package/dist/rpc/client.d.ts +3 -2
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ That's the whole loop. There is no host process to start — `appduct` auto-spaw
|
|
|
42
42
|
| `appduct tools [selector] [name] [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` | list a session's tools (one call signature + description per line), or show one tool's full schema |
|
|
43
43
|
| `appduct tools [selector] --groups` | list a session's tool groups with their tool counts |
|
|
44
44
|
| `appduct invoke [selector] <tool> --input '<json>' [--timeout <ms>]` | call a tool |
|
|
45
|
-
| `appduct events [selector] [--follow] [--since <cursor>]` | stream
|
|
45
|
+
| `appduct events [selector] [--follow] [--since <cursor>]` | stream the events the app posts with `postEvent` (default), or one-shot pull the ones retained since `<cursor>` (`--since`); `--json` emits NDJSON |
|
|
46
46
|
| `appduct revoke [selector]` | revoke a session |
|
|
47
47
|
| `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
|
|
48
48
|
| `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
|
|
@@ -232,7 +232,7 @@ import { connect } from "appduct/client";
|
|
|
232
232
|
// auto-spawns the daemon and picks the single session, or pass { selector: "pixel-8" } to target one explicitly
|
|
233
233
|
const app = await connect();
|
|
234
234
|
|
|
235
|
-
await app.tools(); //
|
|
235
|
+
await app.tools(); // ListedToolDescriptor[]; an ungrouped tool's group is null
|
|
236
236
|
const { total } = await app.call("sum", { a: 2, b: 3 });
|
|
237
237
|
const { payload } = await app.waitForEvent("checkout_done", { timeoutMs: 5_000 });
|
|
238
238
|
app.close();
|
|
@@ -289,13 +289,13 @@ This writes an unencrypted PEM private key (PKCS#8) to `<state-dir>/key.pem` by
|
|
|
289
289
|
|
|
290
290
|
## Daemon lifecycle
|
|
291
291
|
|
|
292
|
-
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
|
|
292
|
+
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `events.log`, `events.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
|
|
293
293
|
|
|
294
294
|
Use `appduct daemon status` to see what's running (version, pid, `wssPort`, pinned keys, live sessions, effective policy, and the audit log's retention window, file count, size, and failure counters) and `appduct daemon stop` to shut it down explicitly.
|
|
295
295
|
|
|
296
296
|
Because the daemon outlives the CLI that started it, upgrading Appduct would otherwise leave the old daemon serving your commands. Every CLI/MCP process compares its version with the daemon's on its first command and replaces a stale daemon when nothing would be lost; when there is something at stake — connected sessions, or an unexpired link nobody has scanned yet — the command stops instead, naming both versions, until you run `appduct daemon stop` or pass `--daemon-restart`. A daemon newer than your CLI is left alone with a warning, so a project-local install never downgrades a global one's daemon.
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
No log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned, and `events.log` is rotated to `events.log.1` as soon as it passes `eventsLogMaxBytes` (default 10 MiB). `events.log` records the daemon's own events (links, sessions, tool registries and tool calls), one JSON line each, for debugging Appduct itself; your app's events are never written to it. All three are `config.json` keys — see [`ARCHITECTURE.md` §3][architecture].
|
|
299
299
|
|
|
300
300
|
## Release gate: `appduct doctor`
|
|
301
301
|
|
package/dist/bin.js
CHANGED
|
@@ -142,7 +142,7 @@ var createCli = () => {
|
|
|
142
142
|
"--timeout <ms>",
|
|
143
143
|
"Call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
|
|
144
144
|
);
|
|
145
|
-
cli.command("events [selector]", "Stream
|
|
145
|
+
cli.command("events [selector]", "Stream the events the app posts until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print the app events retained since this cursor instead of streaming live.");
|
|
146
146
|
cli.command("revoke [selector]", "Revoke a session.");
|
|
147
147
|
cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
|
|
148
148
|
"--scheme <scheme>",
|
|
@@ -166,6 +166,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
166
166
|
"eventBufferSize",
|
|
167
167
|
"auditRetentionDays",
|
|
168
168
|
"daemonLogMaxBytes",
|
|
169
|
+
"eventsLogMaxBytes",
|
|
169
170
|
"policy",
|
|
170
171
|
"advertisedIp",
|
|
171
172
|
"scheme",
|
|
@@ -225,6 +226,7 @@ var defaultConfig = (paths) => {
|
|
|
225
226
|
eventBufferSize: 256,
|
|
226
227
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
227
228
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
229
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
228
230
|
restartDaemonOnVersionMismatch: false,
|
|
229
231
|
policy: {
|
|
230
232
|
default: "allow",
|
|
@@ -287,6 +289,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
287
289
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
288
290
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
289
291
|
}
|
|
292
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
293
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
294
|
+
}
|
|
290
295
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
291
296
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
292
297
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -335,6 +340,7 @@ var getStateDirPaths = (stateDir) => {
|
|
|
335
340
|
socketPath: join3(stateDir, "daemon.sock"),
|
|
336
341
|
pidFilePath: join3(stateDir, "daemon.pid"),
|
|
337
342
|
logFilePath: join3(stateDir, "daemon.log"),
|
|
343
|
+
eventsLogPath: join3(stateDir, "events.log"),
|
|
338
344
|
keyPath: join3(stateDir, "key.pem"),
|
|
339
345
|
configPath: join3(stateDir, "config.json"),
|
|
340
346
|
auditDir: join3(stateDir, "audit"),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* the thin-client shapes for the v2 command table.
|
|
5
5
|
*/
|
|
6
6
|
import type { OpenTarget } from "./open-target.js";
|
|
7
|
-
import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary,
|
|
7
|
+
import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
|
|
8
8
|
/**
|
|
9
9
|
* CLI-domain error classes, used only to pick a sysexits-style exit code (see `errors.ts`).
|
|
10
10
|
* `CliError.type` itself is *not* restricted to this union: RPC/app-originated failures preserve
|
|
@@ -129,9 +129,10 @@ export type ToolGroupsListing = {
|
|
|
129
129
|
/** Every tool in the session, grouped or not. */
|
|
130
130
|
total: number;
|
|
131
131
|
};
|
|
132
|
-
/** `appduct tools`: a listing, a groups summary (`--groups`), or a single
|
|
133
|
-
*
|
|
134
|
-
|
|
132
|
+
/** `appduct tools`: a listing, a groups summary (`--groups`), or a single entry when a tool name
|
|
133
|
+
* resolved to a detail lookup. The detail form is the daemon's own listing entry, picked out of
|
|
134
|
+
* `tools.list` — not a registration, so its `group` is `null` for an ungrouped tool. */
|
|
135
|
+
export type ToolsCommandData = ToolsListing | ToolGroupsListing | ToolsListEntry;
|
|
135
136
|
/** `appduct invoke`: the tool's raw result payload, printed as-is. */
|
|
136
137
|
export type InvokeCommandData = unknown;
|
|
137
138
|
export type RevokeCommandData = {
|
|
@@ -139,6 +139,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
139
139
|
"eventBufferSize",
|
|
140
140
|
"auditRetentionDays",
|
|
141
141
|
"daemonLogMaxBytes",
|
|
142
|
+
"eventsLogMaxBytes",
|
|
142
143
|
"policy",
|
|
143
144
|
"advertisedIp",
|
|
144
145
|
"scheme",
|
|
@@ -198,6 +199,7 @@ var defaultConfig = (paths) => {
|
|
|
198
199
|
eventBufferSize: 256,
|
|
199
200
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
200
201
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
202
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
201
203
|
restartDaemonOnVersionMismatch: false,
|
|
202
204
|
policy: {
|
|
203
205
|
default: "allow",
|
|
@@ -260,6 +262,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
260
262
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
261
263
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
262
264
|
}
|
|
265
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
266
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
267
|
+
}
|
|
263
268
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
264
269
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
265
270
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -609,7 +614,6 @@ var createCallsManager = (options) => {
|
|
|
609
614
|
|
|
610
615
|
// src/daemon/event-bus.ts
|
|
611
616
|
var TERMINAL_EVENT_KINDS = /* @__PURE__ */ new Set(["session_expired", "session_revoked"]);
|
|
612
|
-
var EXCLUDED_FROM_RETENTION = /* @__PURE__ */ new Set(["tool_call_progress"]);
|
|
613
617
|
var createEventBus = (options = {}) => {
|
|
614
618
|
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
615
619
|
const bufferSize = options.bufferSize ?? 256;
|
|
@@ -640,7 +644,7 @@ var createEventBus = (options = {}) => {
|
|
|
640
644
|
if (TERMINAL_EVENT_KINDS.has(notification.kind)) {
|
|
641
645
|
buffers.delete(sessionId);
|
|
642
646
|
cursors.delete(sessionId);
|
|
643
|
-
} else if (
|
|
647
|
+
} else if (notification.kind === "app_event") {
|
|
644
648
|
appendToBuffer(sessionId, notification);
|
|
645
649
|
}
|
|
646
650
|
}
|
|
@@ -664,10 +668,6 @@ var createEventBus = (options = {}) => {
|
|
|
664
668
|
if (query.since !== void 0) {
|
|
665
669
|
events = events.filter((event) => event.seq > query.since);
|
|
666
670
|
}
|
|
667
|
-
if (query.kinds !== void 0) {
|
|
668
|
-
const kinds = new Set(query.kinds);
|
|
669
|
-
events = events.filter((event) => kinds.has(event.kind));
|
|
670
|
-
}
|
|
671
671
|
if (query.limit !== void 0 && events.length > query.limit) {
|
|
672
672
|
events = events.slice(0, query.limit);
|
|
673
673
|
}
|
|
@@ -681,6 +681,28 @@ var createEventBus = (options = {}) => {
|
|
|
681
681
|
};
|
|
682
682
|
};
|
|
683
683
|
|
|
684
|
+
// src/daemon/events-log.ts
|
|
685
|
+
var createEventsLog = (options) => {
|
|
686
|
+
const { file, maxBytes, warn } = options;
|
|
687
|
+
let queue = Promise.resolve();
|
|
688
|
+
return {
|
|
689
|
+
record: (event) => {
|
|
690
|
+
const line = JSON.stringify(event);
|
|
691
|
+
queue = queue.then(async () => {
|
|
692
|
+
try {
|
|
693
|
+
await file.append(line);
|
|
694
|
+
if (await file.size() > maxBytes) {
|
|
695
|
+
await file.rotate();
|
|
696
|
+
}
|
|
697
|
+
} catch (error) {
|
|
698
|
+
warn(`appduct daemon: failed to write events.log: ${error.message}`);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
},
|
|
702
|
+
flush: () => queue
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
|
|
684
706
|
// src/daemon/listener.ts
|
|
685
707
|
import { createServer as createHttpsServer } from "node:https";
|
|
686
708
|
import { isSessionClaimMessage, isSessionResumeMessage, isSessionBoundMessage } from "@appduct/shared";
|
|
@@ -1409,6 +1431,37 @@ var acquirePidfile = async (pidFilePath, options = {}) => {
|
|
|
1409
1431
|
throw new Error(`Failed to acquire the Appduct daemon pidfile at "${pidFilePath}".`);
|
|
1410
1432
|
};
|
|
1411
1433
|
|
|
1434
|
+
// src/daemon/node-append-only-file.ts
|
|
1435
|
+
import { open as open3, rename, stat as stat2 } from "node:fs/promises";
|
|
1436
|
+
var NodeAppendOnlyFile = class {
|
|
1437
|
+
constructor(path) {
|
|
1438
|
+
this.path = path;
|
|
1439
|
+
}
|
|
1440
|
+
async append(line) {
|
|
1441
|
+
const handle = await open3(this.path, "a", 384);
|
|
1442
|
+
try {
|
|
1443
|
+
await handle.appendFile(`${line}
|
|
1444
|
+
`);
|
|
1445
|
+
await handle.chmod(384);
|
|
1446
|
+
} finally {
|
|
1447
|
+
await handle.close();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async size() {
|
|
1451
|
+
try {
|
|
1452
|
+
return (await stat2(this.path)).size;
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
if (error.code === "ENOENT") {
|
|
1455
|
+
return 0;
|
|
1456
|
+
}
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
async rotate() {
|
|
1461
|
+
await rename(this.path, `${this.path}.1`);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1412
1465
|
// src/daemon/rpc-server.ts
|
|
1413
1466
|
import { createServer } from "node:net";
|
|
1414
1467
|
import { chmod as chmod2, rm as rm3 } from "node:fs/promises";
|
|
@@ -1594,6 +1647,7 @@ var getStateDirPaths = (stateDir) => {
|
|
|
1594
1647
|
socketPath: join4(stateDir, "daemon.sock"),
|
|
1595
1648
|
pidFilePath: join4(stateDir, "daemon.pid"),
|
|
1596
1649
|
logFilePath: join4(stateDir, "daemon.log"),
|
|
1650
|
+
eventsLogPath: join4(stateDir, "events.log"),
|
|
1597
1651
|
keyPath: join4(stateDir, "key.pem"),
|
|
1598
1652
|
configPath: join4(stateDir, "config.json"),
|
|
1599
1653
|
auditDir: join4(stateDir, "audit"),
|
|
@@ -1612,7 +1666,7 @@ var ensureStateDir = async (stateDir) => {
|
|
|
1612
1666
|
};
|
|
1613
1667
|
|
|
1614
1668
|
// src/daemon/tls.ts
|
|
1615
|
-
import { stat as
|
|
1669
|
+
import { stat as stat3, readFile as readFile3 } from "node:fs/promises";
|
|
1616
1670
|
|
|
1617
1671
|
// src/host-certificate.ts
|
|
1618
1672
|
import "reflect-metadata";
|
|
@@ -1789,7 +1843,7 @@ var generateHostCertificate = async (keyPem, advertisedIp) => {
|
|
|
1789
1843
|
// src/key-material.ts
|
|
1790
1844
|
import { generateKeyPairSync } from "node:crypto";
|
|
1791
1845
|
import { dirname as dirname2 } from "node:path";
|
|
1792
|
-
import { mkdir as mkdir3, rename, rm as rm4, writeFile } from "node:fs/promises";
|
|
1846
|
+
import { mkdir as mkdir3, rename as rename2, rm as rm4, writeFile } from "node:fs/promises";
|
|
1793
1847
|
var generatePrivateKeyPem = () => {
|
|
1794
1848
|
const { privateKey } = generateKeyPairSync("rsa", {
|
|
1795
1849
|
modulusLength: 2048,
|
|
@@ -1809,7 +1863,7 @@ var writePrivateKeyAtomically = async (path, keyPem) => {
|
|
|
1809
1863
|
mode: 384,
|
|
1810
1864
|
flag: "wx"
|
|
1811
1865
|
});
|
|
1812
|
-
await
|
|
1866
|
+
await rename2(temporaryPath, path);
|
|
1813
1867
|
} catch (error) {
|
|
1814
1868
|
await rm4(temporaryPath, { force: true }).catch(() => {
|
|
1815
1869
|
});
|
|
@@ -1825,7 +1879,7 @@ var HostKeyError = class extends Error {
|
|
|
1825
1879
|
}
|
|
1826
1880
|
};
|
|
1827
1881
|
var loadHostKeyPem = async (keyPath) => {
|
|
1828
|
-
const fileStat = await
|
|
1882
|
+
const fileStat = await stat3(keyPath);
|
|
1829
1883
|
if ((fileStat.mode & 63) !== 0) {
|
|
1830
1884
|
throw new HostKeyError(
|
|
1831
1885
|
`Appduct host key at "${keyPath}" is group- or world-readable (mode ${(fileStat.mode & 511).toString(8)}). Refusing to load it \u2014 run "chmod 600 ${keyPath}" or re-run "appduct keygen".`
|
|
@@ -2030,14 +2084,6 @@ var asEventsSinceParams = (params) => {
|
|
|
2030
2084
|
if (since !== void 0 && (typeof since !== "number" || !Number.isInteger(since) || since < 0)) {
|
|
2031
2085
|
throw new RpcApplicationError("invalid_request", '"since" must be a non-negative integer.');
|
|
2032
2086
|
}
|
|
2033
|
-
const kindsRaw = record.kinds;
|
|
2034
|
-
let kinds;
|
|
2035
|
-
if (kindsRaw !== void 0) {
|
|
2036
|
-
if (!Array.isArray(kindsRaw) || !kindsRaw.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
|
|
2037
|
-
throw new RpcApplicationError("invalid_request", '"kinds" must be an array of known event kinds.');
|
|
2038
|
-
}
|
|
2039
|
-
kinds = kindsRaw;
|
|
2040
|
-
}
|
|
2041
2087
|
const limit = record.limit;
|
|
2042
2088
|
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
2043
2089
|
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
@@ -2045,7 +2091,6 @@ var asEventsSinceParams = (params) => {
|
|
|
2045
2091
|
return {
|
|
2046
2092
|
selector,
|
|
2047
2093
|
since,
|
|
2048
|
-
kinds,
|
|
2049
2094
|
limit
|
|
2050
2095
|
};
|
|
2051
2096
|
};
|
|
@@ -2080,10 +2125,19 @@ var startDaemon = async (options) => {
|
|
|
2080
2125
|
retentionDays: config.auditRetentionDays,
|
|
2081
2126
|
warn: options.warn
|
|
2082
2127
|
});
|
|
2128
|
+
const eventsLog = createEventsLog({
|
|
2129
|
+
file: new NodeAppendOnlyFile(paths.eventsLogPath),
|
|
2130
|
+
maxBytes: config.eventsLogMaxBytes,
|
|
2131
|
+
warn: options.warn ?? ((message) => {
|
|
2132
|
+
process.stderr.write(`${message}
|
|
2133
|
+
`);
|
|
2134
|
+
})
|
|
2135
|
+
});
|
|
2083
2136
|
let auditPruneInterval;
|
|
2084
2137
|
let pidfile;
|
|
2085
2138
|
let server;
|
|
2086
2139
|
let listener;
|
|
2140
|
+
let tlsManager;
|
|
2087
2141
|
let sessionManager;
|
|
2088
2142
|
let callsManager;
|
|
2089
2143
|
let eventBus;
|
|
@@ -2110,6 +2164,7 @@ var startDaemon = async (options) => {
|
|
|
2110
2164
|
await pidfile?.release();
|
|
2111
2165
|
await rm5(getSocketPath(paths), { force: true });
|
|
2112
2166
|
await auditLogger.flush();
|
|
2167
|
+
await eventsLog.flush();
|
|
2113
2168
|
} finally {
|
|
2114
2169
|
resolveExited();
|
|
2115
2170
|
}
|
|
@@ -2122,8 +2177,14 @@ var startDaemon = async (options) => {
|
|
|
2122
2177
|
});
|
|
2123
2178
|
eventBus = createEventBus({ clock, bufferSize: config.eventBufferSize });
|
|
2124
2179
|
const activeEventBus = eventBus;
|
|
2180
|
+
activeEventBus.subscribe((event) => {
|
|
2181
|
+
if (event.kind !== "app_event") {
|
|
2182
|
+
eventsLog.record(event);
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2125
2185
|
const detectAddress = options.detectAddress ?? (() => detectAdvertisedAddress({ override: config.advertisedIp }));
|
|
2126
2186
|
const tls = await createTlsManager({ keyPath: config.keyPath, detectAddress, warn: options.warn });
|
|
2187
|
+
tlsManager = tls;
|
|
2127
2188
|
sessionManager = createSessionManager({
|
|
2128
2189
|
graceSeconds: config.graceSeconds,
|
|
2129
2190
|
keepaliveIntervalSeconds: config.keepaliveIntervalSeconds,
|
|
@@ -2367,9 +2428,9 @@ var startDaemon = async (options) => {
|
|
|
2367
2428
|
return { ok: true };
|
|
2368
2429
|
},
|
|
2369
2430
|
[RPC_METHODS.eventsSince]: (params) => {
|
|
2370
|
-
const { selector, since,
|
|
2431
|
+
const { selector, since, limit } = asEventsSinceParams(params);
|
|
2371
2432
|
const resolved = activeSessionManager.describe(selector);
|
|
2372
|
-
return activeEventBus.since(resolved.sessionId, { since,
|
|
2433
|
+
return activeEventBus.since(resolved.sessionId, { since, limit });
|
|
2373
2434
|
}
|
|
2374
2435
|
}
|
|
2375
2436
|
});
|
|
@@ -2395,6 +2456,7 @@ var startDaemon = async (options) => {
|
|
|
2395
2456
|
startedAt,
|
|
2396
2457
|
server,
|
|
2397
2458
|
listener,
|
|
2459
|
+
tls: tlsManager,
|
|
2398
2460
|
eventBus,
|
|
2399
2461
|
exited,
|
|
2400
2462
|
shutdown
|
|
@@ -9,7 +9,7 @@ import { join as join3 } from "node:path";
|
|
|
9
9
|
import { RPC_METHODS } from "@appduct/shared";
|
|
10
10
|
|
|
11
11
|
// src/daemon/log-rotation.ts
|
|
12
|
-
import { chmod, rename, stat } from "node:fs/promises";
|
|
12
|
+
import { chmod as chmod2, rename, stat } from "node:fs/promises";
|
|
13
13
|
|
|
14
14
|
// src/daemon/config.ts
|
|
15
15
|
import { readFile } from "node:fs/promises";
|
|
@@ -22,6 +22,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
22
22
|
"eventBufferSize",
|
|
23
23
|
"auditRetentionDays",
|
|
24
24
|
"daemonLogMaxBytes",
|
|
25
|
+
"eventsLogMaxBytes",
|
|
25
26
|
"policy",
|
|
26
27
|
"advertisedIp",
|
|
27
28
|
"scheme",
|
|
@@ -81,6 +82,7 @@ var defaultConfig = (paths) => {
|
|
|
81
82
|
eventBufferSize: 256,
|
|
82
83
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
83
84
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
85
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
84
86
|
restartDaemonOnVersionMismatch: false,
|
|
85
87
|
policy: {
|
|
86
88
|
default: "allow",
|
|
@@ -143,6 +145,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
143
145
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
144
146
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
145
147
|
}
|
|
148
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
149
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
150
|
+
}
|
|
146
151
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
147
152
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
148
153
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -236,18 +241,27 @@ var isSocketConnectable = (socketPath) => {
|
|
|
236
241
|
|
|
237
242
|
// src/daemon/state-dir.ts
|
|
238
243
|
import { join } from "node:path";
|
|
244
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
239
245
|
var getStateDirPaths = (stateDir) => {
|
|
240
246
|
return {
|
|
241
247
|
root: stateDir,
|
|
242
248
|
socketPath: join(stateDir, "daemon.sock"),
|
|
243
249
|
pidFilePath: join(stateDir, "daemon.pid"),
|
|
244
250
|
logFilePath: join(stateDir, "daemon.log"),
|
|
251
|
+
eventsLogPath: join(stateDir, "events.log"),
|
|
245
252
|
keyPath: join(stateDir, "key.pem"),
|
|
246
253
|
configPath: join(stateDir, "config.json"),
|
|
247
254
|
auditDir: join(stateDir, "audit"),
|
|
248
255
|
spawnLockPath: join(stateDir, "daemon.spawn.lock")
|
|
249
256
|
};
|
|
250
257
|
};
|
|
258
|
+
var ensureStateDir = async (stateDir) => {
|
|
259
|
+
const paths = getStateDirPaths(stateDir);
|
|
260
|
+
await mkdir(paths.auditDir, { recursive: true });
|
|
261
|
+
await chmod(paths.root, 448);
|
|
262
|
+
await chmod(paths.auditDir, 448);
|
|
263
|
+
return paths;
|
|
264
|
+
};
|
|
251
265
|
|
|
252
266
|
// src/daemon/log-rotation.ts
|
|
253
267
|
var STATE_FILE_MODE = 384;
|
|
@@ -280,7 +294,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
280
294
|
const backupPath = daemonLogBackupPath(options.logFilePath);
|
|
281
295
|
await rename(options.logFilePath, backupPath);
|
|
282
296
|
try {
|
|
283
|
-
await
|
|
297
|
+
await chmod2(backupPath, STATE_FILE_MODE);
|
|
284
298
|
} catch (error) {
|
|
285
299
|
warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
|
|
286
300
|
}
|
|
@@ -296,7 +310,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
296
310
|
var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
|
|
297
311
|
`)) => {
|
|
298
312
|
try {
|
|
299
|
-
await
|
|
313
|
+
await chmod2(logFilePath, STATE_FILE_MODE);
|
|
300
314
|
return true;
|
|
301
315
|
} catch (error) {
|
|
302
316
|
warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
|
|
@@ -637,6 +651,7 @@ var acquireSpawnLock = async (lockPath) => {
|
|
|
637
651
|
return false;
|
|
638
652
|
};
|
|
639
653
|
var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
|
|
654
|
+
await ensureStateDir(paths.root);
|
|
640
655
|
const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
|
|
641
656
|
let lockWaitTimedOut = false;
|
|
642
657
|
if (acquiredLock) {
|
|
@@ -53,7 +53,7 @@ import { join as join4 } from "node:path";
|
|
|
53
53
|
import { RPC_METHODS } from "@appduct/shared";
|
|
54
54
|
|
|
55
55
|
// src/daemon/log-rotation.ts
|
|
56
|
-
import { chmod, rename, stat } from "node:fs/promises";
|
|
56
|
+
import { chmod as chmod2, rename, stat } from "node:fs/promises";
|
|
57
57
|
|
|
58
58
|
// src/daemon/config.ts
|
|
59
59
|
import { readFile } from "node:fs/promises";
|
|
@@ -66,6 +66,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
66
66
|
"eventBufferSize",
|
|
67
67
|
"auditRetentionDays",
|
|
68
68
|
"daemonLogMaxBytes",
|
|
69
|
+
"eventsLogMaxBytes",
|
|
69
70
|
"policy",
|
|
70
71
|
"advertisedIp",
|
|
71
72
|
"scheme",
|
|
@@ -125,6 +126,7 @@ var defaultConfig = (paths) => {
|
|
|
125
126
|
eventBufferSize: 256,
|
|
126
127
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
127
128
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
129
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
128
130
|
restartDaemonOnVersionMismatch: false,
|
|
129
131
|
policy: {
|
|
130
132
|
default: "allow",
|
|
@@ -187,6 +189,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
187
189
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
188
190
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
189
191
|
}
|
|
192
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
193
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
194
|
+
}
|
|
190
195
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
191
196
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
192
197
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -280,18 +285,27 @@ var isSocketConnectable = (socketPath) => {
|
|
|
280
285
|
|
|
281
286
|
// src/daemon/state-dir.ts
|
|
282
287
|
import { join as join3 } from "node:path";
|
|
288
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
283
289
|
var getStateDirPaths = (stateDir) => {
|
|
284
290
|
return {
|
|
285
291
|
root: stateDir,
|
|
286
292
|
socketPath: join3(stateDir, "daemon.sock"),
|
|
287
293
|
pidFilePath: join3(stateDir, "daemon.pid"),
|
|
288
294
|
logFilePath: join3(stateDir, "daemon.log"),
|
|
295
|
+
eventsLogPath: join3(stateDir, "events.log"),
|
|
289
296
|
keyPath: join3(stateDir, "key.pem"),
|
|
290
297
|
configPath: join3(stateDir, "config.json"),
|
|
291
298
|
auditDir: join3(stateDir, "audit"),
|
|
292
299
|
spawnLockPath: join3(stateDir, "daemon.spawn.lock")
|
|
293
300
|
};
|
|
294
301
|
};
|
|
302
|
+
var ensureStateDir = async (stateDir) => {
|
|
303
|
+
const paths = getStateDirPaths(stateDir);
|
|
304
|
+
await mkdir(paths.auditDir, { recursive: true });
|
|
305
|
+
await chmod(paths.root, 448);
|
|
306
|
+
await chmod(paths.auditDir, 448);
|
|
307
|
+
return paths;
|
|
308
|
+
};
|
|
295
309
|
|
|
296
310
|
// src/daemon/log-rotation.ts
|
|
297
311
|
var STATE_FILE_MODE = 384;
|
|
@@ -324,7 +338,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
324
338
|
const backupPath = daemonLogBackupPath(options.logFilePath);
|
|
325
339
|
await rename(options.logFilePath, backupPath);
|
|
326
340
|
try {
|
|
327
|
-
await
|
|
341
|
+
await chmod2(backupPath, STATE_FILE_MODE);
|
|
328
342
|
} catch (error) {
|
|
329
343
|
warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
|
|
330
344
|
}
|
|
@@ -340,7 +354,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
340
354
|
var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
|
|
341
355
|
`)) => {
|
|
342
356
|
try {
|
|
343
|
-
await
|
|
357
|
+
await chmod2(logFilePath, STATE_FILE_MODE);
|
|
344
358
|
return true;
|
|
345
359
|
} catch (error) {
|
|
346
360
|
warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
|
|
@@ -646,6 +660,7 @@ var acquireSpawnLock = async (lockPath) => {
|
|
|
646
660
|
return false;
|
|
647
661
|
};
|
|
648
662
|
var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
|
|
663
|
+
await ensureStateDir(paths.root);
|
|
649
664
|
const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
|
|
650
665
|
let lockWaitTimedOut = false;
|
|
651
666
|
if (acquiredLock) {
|
|
@@ -42,18 +42,27 @@ var readPidFromFile = async (pidFilePath) => {
|
|
|
42
42
|
|
|
43
43
|
// src/daemon/state-dir.ts
|
|
44
44
|
import { join } from "node:path";
|
|
45
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
45
46
|
var getStateDirPaths = (stateDir) => {
|
|
46
47
|
return {
|
|
47
48
|
root: stateDir,
|
|
48
49
|
socketPath: join(stateDir, "daemon.sock"),
|
|
49
50
|
pidFilePath: join(stateDir, "daemon.pid"),
|
|
50
51
|
logFilePath: join(stateDir, "daemon.log"),
|
|
52
|
+
eventsLogPath: join(stateDir, "events.log"),
|
|
51
53
|
keyPath: join(stateDir, "key.pem"),
|
|
52
54
|
configPath: join(stateDir, "config.json"),
|
|
53
55
|
auditDir: join(stateDir, "audit"),
|
|
54
56
|
spawnLockPath: join(stateDir, "daemon.spawn.lock")
|
|
55
57
|
};
|
|
56
58
|
};
|
|
59
|
+
var ensureStateDir = async (stateDir) => {
|
|
60
|
+
const paths = getStateDirPaths(stateDir);
|
|
61
|
+
await mkdir(paths.auditDir, { recursive: true });
|
|
62
|
+
await chmod(paths.root, 448);
|
|
63
|
+
await chmod(paths.auditDir, 448);
|
|
64
|
+
return paths;
|
|
65
|
+
};
|
|
57
66
|
|
|
58
67
|
// src/rpc/client.ts
|
|
59
68
|
import { connect as connect2 } from "node:net";
|
|
@@ -63,7 +72,7 @@ import { join as join3 } from "node:path";
|
|
|
63
72
|
import { RPC_METHODS } from "@appduct/shared";
|
|
64
73
|
|
|
65
74
|
// src/daemon/log-rotation.ts
|
|
66
|
-
import { chmod, rename, stat } from "node:fs/promises";
|
|
75
|
+
import { chmod as chmod2, rename, stat } from "node:fs/promises";
|
|
67
76
|
|
|
68
77
|
// src/daemon/config.ts
|
|
69
78
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
@@ -76,6 +85,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
76
85
|
"eventBufferSize",
|
|
77
86
|
"auditRetentionDays",
|
|
78
87
|
"daemonLogMaxBytes",
|
|
88
|
+
"eventsLogMaxBytes",
|
|
79
89
|
"policy",
|
|
80
90
|
"advertisedIp",
|
|
81
91
|
"scheme",
|
|
@@ -135,6 +145,7 @@ var defaultConfig = (paths) => {
|
|
|
135
145
|
eventBufferSize: 256,
|
|
136
146
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
137
147
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
148
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
138
149
|
restartDaemonOnVersionMismatch: false,
|
|
139
150
|
policy: {
|
|
140
151
|
default: "allow",
|
|
@@ -197,6 +208,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
197
208
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
198
209
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
199
210
|
}
|
|
211
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
212
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
213
|
+
}
|
|
200
214
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
201
215
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
202
216
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -280,7 +294,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
280
294
|
const backupPath = daemonLogBackupPath(options.logFilePath);
|
|
281
295
|
await rename(options.logFilePath, backupPath);
|
|
282
296
|
try {
|
|
283
|
-
await
|
|
297
|
+
await chmod2(backupPath, STATE_FILE_MODE);
|
|
284
298
|
} catch (error) {
|
|
285
299
|
warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
|
|
286
300
|
}
|
|
@@ -296,7 +310,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
296
310
|
var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
|
|
297
311
|
`)) => {
|
|
298
312
|
try {
|
|
299
|
-
await
|
|
313
|
+
await chmod2(logFilePath, STATE_FILE_MODE);
|
|
300
314
|
return true;
|
|
301
315
|
} catch (error) {
|
|
302
316
|
warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
|
|
@@ -637,6 +651,7 @@ var acquireSpawnLock = async (lockPath) => {
|
|
|
637
651
|
return false;
|
|
638
652
|
};
|
|
639
653
|
var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
|
|
654
|
+
await ensureStateDir(paths.root);
|
|
640
655
|
const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
|
|
641
656
|
let lockWaitTimedOut = false;
|
|
642
657
|
if (acquiredLock) {
|