git-daemon 0.1.0 → 0.1.2
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 +14 -7
- package/config.schema.json +1 -1
- package/design.md +3 -3
- package/dist/app.js +306 -0
- package/dist/approvals.js +27 -0
- package/dist/config.js +107 -0
- package/dist/context.js +58 -0
- package/dist/daemon.js +29 -0
- package/dist/deps.js +101 -0
- package/dist/errors.js +43 -0
- package/dist/git.js +156 -0
- package/dist/jobs.js +163 -0
- package/dist/logger.js +77 -0
- package/dist/os.js +42 -0
- package/dist/pairing.js +41 -0
- package/dist/process.js +46 -0
- package/dist/security.js +73 -0
- package/dist/tokens.js +88 -0
- package/dist/tools.js +43 -0
- package/dist/types.js +2 -0
- package/dist/validation.js +62 -0
- package/dist/workspace.js +79 -0
- package/openapi.yaml +1 -1
- package/package.json +2 -1
- package/src/config.ts +1 -1
- package/src/daemon.ts +14 -0
- package/src/jobs.ts +3 -3
- package/src/logger.ts +26 -4
- package/src/process.ts +3 -2
- package/src/types.ts +20 -17
- package/src/typings/pino-pretty.d.ts +9 -0
- package/tests/app.test.js +103 -0
- package/tsconfig.json +2 -1
- package/vitest.config.js +9 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Git Daemon
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
|
+
[](https://www.npmjs.com/package/git-daemon)
|
|
4
5
|
|
|
5
6
|
Git Daemon is a local Node.js service that exposes a small, authenticated HTTP API for a trusted web UI to perform Git and developer convenience actions on your machine. It is designed to run on `127.0.0.1` only, enforce a strict Origin allowlist, and sandbox all filesystem access to a configured workspace root.
|
|
6
7
|
|
|
@@ -38,7 +39,13 @@ npm install
|
|
|
38
39
|
npm run daemon
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
The daemon listens on `http://127.0.0.1:
|
|
42
|
+
The daemon listens on `http://127.0.0.1:8790` by default.
|
|
43
|
+
|
|
44
|
+
Verbose logging options:
|
|
45
|
+
|
|
46
|
+
- `GIT_DAEMON_LOG_STDOUT=1` to mirror logs to stdout
|
|
47
|
+
- `GIT_DAEMON_LOG_PRETTY=0` to disable pretty formatting when stdout is enabled
|
|
48
|
+
- `GIT_DAEMON_LOG_LEVEL=debug` to increase verbosity
|
|
42
49
|
|
|
43
50
|
## Pairing flow
|
|
44
51
|
|
|
@@ -50,7 +57,7 @@ Pairing is required before using protected endpoints.
|
|
|
50
57
|
curl -H "Origin: https://app.example.com" \
|
|
51
58
|
-H "Content-Type: application/json" \
|
|
52
59
|
-d '{"step":"start"}' \
|
|
53
|
-
http://127.0.0.1:
|
|
60
|
+
http://127.0.0.1:8790/v1/pair
|
|
54
61
|
```
|
|
55
62
|
|
|
56
63
|
2. Confirm pairing with the code:
|
|
@@ -59,7 +66,7 @@ curl -H "Origin: https://app.example.com" \
|
|
|
59
66
|
curl -H "Origin: https://app.example.com" \
|
|
60
67
|
-H "Content-Type: application/json" \
|
|
61
68
|
-d '{"step":"confirm","code":"<CODE>"}' \
|
|
62
|
-
http://127.0.0.1:
|
|
69
|
+
http://127.0.0.1:8790/v1/pair
|
|
63
70
|
```
|
|
64
71
|
|
|
65
72
|
The response includes `accessToken` to use as `Authorization: Bearer <token>`.
|
|
@@ -70,7 +77,7 @@ Check meta:
|
|
|
70
77
|
|
|
71
78
|
```bash
|
|
72
79
|
curl -H "Origin: https://app.example.com" \
|
|
73
|
-
http://127.0.0.1:
|
|
80
|
+
http://127.0.0.1:8790/v1/meta
|
|
74
81
|
```
|
|
75
82
|
|
|
76
83
|
Clone a repo (job):
|
|
@@ -81,7 +88,7 @@ curl -X POST \
|
|
|
81
88
|
-H "Authorization: Bearer <TOKEN>" \
|
|
82
89
|
-H "Content-Type: application/json" \
|
|
83
90
|
-d '{"repoUrl":"git@github.com:owner/repo.git","destRelative":"owner/repo"}' \
|
|
84
|
-
http://127.0.0.1:
|
|
91
|
+
http://127.0.0.1:8790/v1/git/clone
|
|
85
92
|
```
|
|
86
93
|
|
|
87
94
|
Stream job logs (SSE):
|
|
@@ -90,7 +97,7 @@ Stream job logs (SSE):
|
|
|
90
97
|
curl -N \
|
|
91
98
|
-H "Origin: https://app.example.com" \
|
|
92
99
|
-H "Authorization: Bearer <TOKEN>" \
|
|
93
|
-
http://127.0.0.1:
|
|
100
|
+
http://127.0.0.1:8790/v1/jobs/<JOB_ID>/stream
|
|
94
101
|
```
|
|
95
102
|
|
|
96
103
|
## Configuration
|
package/config.schema.json
CHANGED
package/design.md
CHANGED
|
@@ -399,7 +399,7 @@ Same, except UI origin is localhost and allowlisted.
|
|
|
399
399
|
|
|
400
400
|
```bash
|
|
401
401
|
curl -H "Origin: https://app.example.com" \
|
|
402
|
-
http://127.0.0.1:
|
|
402
|
+
http://127.0.0.1:8790/v1/meta
|
|
403
403
|
```
|
|
404
404
|
|
|
405
405
|
### Clone
|
|
@@ -410,7 +410,7 @@ curl -X POST \
|
|
|
410
410
|
-H "Authorization: Bearer <TOKEN>" \
|
|
411
411
|
-H "Content-Type: application/json" \
|
|
412
412
|
-d '{"repoUrl":"git@github.com:owner/repo.git","destRelative":"owner/repo"}' \
|
|
413
|
-
http://127.0.0.1:
|
|
413
|
+
http://127.0.0.1:8790/v1/git/clone
|
|
414
414
|
```
|
|
415
415
|
|
|
416
416
|
### Stream job logs (SSE)
|
|
@@ -419,7 +419,7 @@ curl -X POST \
|
|
|
419
419
|
curl -N \
|
|
420
420
|
-H "Origin: https://app.example.com" \
|
|
421
421
|
-H "Authorization: Bearer <TOKEN>" \
|
|
422
|
-
http://127.0.0.1:
|
|
422
|
+
http://127.0.0.1:8790/v1/jobs/<JOB_ID>/stream
|
|
423
423
|
```
|
|
424
424
|
|
|
425
425
|
---
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createApp = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const logger_1 = require("./logger");
|
|
12
|
+
const security_1 = require("./security");
|
|
13
|
+
const errors_1 = require("./errors");
|
|
14
|
+
const validation_1 = require("./validation");
|
|
15
|
+
const workspace_1 = require("./workspace");
|
|
16
|
+
const git_1 = require("./git");
|
|
17
|
+
const deps_1 = require("./deps");
|
|
18
|
+
const os_1 = require("./os");
|
|
19
|
+
const approvals_1 = require("./approvals");
|
|
20
|
+
const parseBody = (schema, body, opts) => {
|
|
21
|
+
const parsed = schema.safeParse(body);
|
|
22
|
+
if (!parsed.success) {
|
|
23
|
+
if (opts?.repoUrl &&
|
|
24
|
+
parsed.error.issues.some((issue) => issue.path.includes("repoUrl"))) {
|
|
25
|
+
throw new errors_1.ApiError(422, (0, errors_1.errorBody)("invalid_repo_url", "Repository URL is invalid."));
|
|
26
|
+
}
|
|
27
|
+
throw new errors_1.ApiError(422, (0, errors_1.errorBody)("internal_error", "Invalid input."));
|
|
28
|
+
}
|
|
29
|
+
return parsed.data;
|
|
30
|
+
};
|
|
31
|
+
const rateLimitHandler = (_req, res) => {
|
|
32
|
+
const body = (0, errors_1.rateLimited)().body;
|
|
33
|
+
res.status(429).json(body);
|
|
34
|
+
};
|
|
35
|
+
const createApp = (ctx) => {
|
|
36
|
+
const app = (0, express_1.default)();
|
|
37
|
+
app.disable("x-powered-by");
|
|
38
|
+
app.use((0, logger_1.createHttpLogger)(ctx.logger));
|
|
39
|
+
app.use(express_1.default.json({ limit: "256kb" }));
|
|
40
|
+
app.use((0, security_1.loopbackGuard)());
|
|
41
|
+
app.use((0, security_1.hostGuard)());
|
|
42
|
+
app.use((0, security_1.originGuard)(ctx.config.originAllowlist));
|
|
43
|
+
app.use("/v1", (0, express_rate_limit_1.default)({
|
|
44
|
+
windowMs: 5 * 60 * 1000,
|
|
45
|
+
max: 300,
|
|
46
|
+
standardHeaders: true,
|
|
47
|
+
legacyHeaders: false,
|
|
48
|
+
handler: rateLimitHandler,
|
|
49
|
+
}));
|
|
50
|
+
app.get("/v1/meta", (_req, res) => {
|
|
51
|
+
const origin = (0, security_1.getOrigin)(_req);
|
|
52
|
+
const pairingRecord = ctx.tokenStore.getActiveToken(origin);
|
|
53
|
+
res.json({
|
|
54
|
+
version: ctx.version,
|
|
55
|
+
build: ctx.build,
|
|
56
|
+
pairing: {
|
|
57
|
+
required: true,
|
|
58
|
+
paired: Boolean(pairingRecord),
|
|
59
|
+
},
|
|
60
|
+
workspace: {
|
|
61
|
+
configured: Boolean(ctx.config.workspaceRoot),
|
|
62
|
+
root: ctx.config.workspaceRoot ?? undefined,
|
|
63
|
+
},
|
|
64
|
+
capabilities: ctx.capabilities,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
app.post("/v1/pair", (0, express_rate_limit_1.default)({
|
|
68
|
+
windowMs: 10 * 60 * 1000,
|
|
69
|
+
max: 10,
|
|
70
|
+
standardHeaders: true,
|
|
71
|
+
legacyHeaders: false,
|
|
72
|
+
handler: rateLimitHandler,
|
|
73
|
+
}), async (req, res, next) => {
|
|
74
|
+
try {
|
|
75
|
+
const origin = (0, security_1.getOrigin)(req);
|
|
76
|
+
const payload = parseBody(validation_1.pairRequestSchema, req.body);
|
|
77
|
+
if (payload.step === "start") {
|
|
78
|
+
res.json(ctx.pairingManager.start(origin));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const response = await ctx.pairingManager.confirm(origin, payload.code);
|
|
82
|
+
if (!response) {
|
|
83
|
+
throw new errors_1.ApiError(422, (0, errors_1.errorBody)("internal_error", "Invalid pairing code."));
|
|
84
|
+
}
|
|
85
|
+
res.json(response);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
next(err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
app.get("/v1/jobs/:id", (0, security_1.authGuard)(ctx.tokenStore), (req, res, next) => {
|
|
92
|
+
try {
|
|
93
|
+
const job = ctx.jobManager.get(req.params.id);
|
|
94
|
+
if (!job) {
|
|
95
|
+
throw (0, errors_1.jobNotFound)();
|
|
96
|
+
}
|
|
97
|
+
res.json(job.snapshot());
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
next(err);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
app.get("/v1/jobs/:id/stream", (0, security_1.authGuard)(ctx.tokenStore), (req, res, next) => {
|
|
104
|
+
const job = ctx.jobManager.get(req.params.id);
|
|
105
|
+
if (!job) {
|
|
106
|
+
next((0, errors_1.jobNotFound)());
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
res.writeHead(200, {
|
|
110
|
+
"Content-Type": "text/event-stream",
|
|
111
|
+
"Cache-Control": "no-cache",
|
|
112
|
+
Connection: "keep-alive",
|
|
113
|
+
"X-Accel-Buffering": "no",
|
|
114
|
+
});
|
|
115
|
+
res.flushHeaders?.();
|
|
116
|
+
const sendEvent = (event) => {
|
|
117
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
118
|
+
};
|
|
119
|
+
for (const event of job.events) {
|
|
120
|
+
sendEvent(event);
|
|
121
|
+
}
|
|
122
|
+
const listener = (event) => sendEvent(event);
|
|
123
|
+
job.emitter.on("event", listener);
|
|
124
|
+
req.on("close", () => {
|
|
125
|
+
job.emitter.off("event", listener);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
app.post("/v1/jobs/:id/cancel", (0, security_1.authGuard)(ctx.tokenStore), (req, res, next) => {
|
|
129
|
+
try {
|
|
130
|
+
const job = ctx.jobManager.get(req.params.id);
|
|
131
|
+
if (!job) {
|
|
132
|
+
throw (0, errors_1.jobNotFound)();
|
|
133
|
+
}
|
|
134
|
+
if (job.state !== "queued" && job.state !== "running") {
|
|
135
|
+
res
|
|
136
|
+
.status(409)
|
|
137
|
+
.json((0, errors_1.errorBody)("internal_error", "Job is not cancellable."));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
ctx.jobManager.cancel(job.id);
|
|
141
|
+
res.json({ accepted: true });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
next(err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
app.post("/v1/git/clone", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
148
|
+
try {
|
|
149
|
+
const payload = parseBody(validation_1.gitCloneRequestSchema, req.body, {
|
|
150
|
+
repoUrl: true,
|
|
151
|
+
});
|
|
152
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
153
|
+
(0, workspace_1.ensureRelative)(payload.destRelative);
|
|
154
|
+
const destPath = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, payload.destRelative, true);
|
|
155
|
+
try {
|
|
156
|
+
await fs_1.promises.access(destPath);
|
|
157
|
+
res
|
|
158
|
+
.status(409)
|
|
159
|
+
.json((0, errors_1.errorBody)("internal_error", "Destination already exists."));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (err.code !== "ENOENT") {
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await fs_1.promises.mkdir(path_1.default.dirname(destPath), { recursive: true });
|
|
168
|
+
const job = ctx.jobManager.enqueue(async (jobCtx) => {
|
|
169
|
+
await (0, git_1.cloneRepo)(jobCtx, workspaceRoot, payload.repoUrl, payload.destRelative, payload.options);
|
|
170
|
+
});
|
|
171
|
+
res.status(202).json({ jobId: job.id });
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
next(err);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
app.post("/v1/git/fetch", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
178
|
+
try {
|
|
179
|
+
const payload = parseBody(validation_1.gitFetchRequestSchema, req.body);
|
|
180
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
181
|
+
await (0, git_1.resolveRepoPath)(workspaceRoot, payload.repoPath);
|
|
182
|
+
const job = ctx.jobManager.enqueue(async (jobCtx) => {
|
|
183
|
+
await (0, git_1.fetchRepo)(jobCtx, workspaceRoot, payload.repoPath, payload.remote, payload.prune);
|
|
184
|
+
});
|
|
185
|
+
res.status(202).json({ jobId: job.id });
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (err instanceof git_1.RepoNotFoundError ||
|
|
189
|
+
err instanceof workspace_1.MissingPathError) {
|
|
190
|
+
next((0, errors_1.repoNotFound)());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
next(err);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
app.get("/v1/git/status", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
197
|
+
try {
|
|
198
|
+
const { repoPath } = parseBody(validation_1.gitStatusQuerySchema, req.query);
|
|
199
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
200
|
+
const status = await (0, git_1.getRepoStatus)(workspaceRoot, repoPath);
|
|
201
|
+
res.json(status);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
if (err instanceof git_1.RepoNotFoundError ||
|
|
205
|
+
err instanceof workspace_1.MissingPathError) {
|
|
206
|
+
next((0, errors_1.repoNotFound)());
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
next(err);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
app.post("/v1/os/open", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
213
|
+
try {
|
|
214
|
+
const origin = (0, security_1.getOrigin)(req);
|
|
215
|
+
const payload = parseBody(validation_1.osOpenRequestSchema, req.body);
|
|
216
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
217
|
+
const resolved = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, payload.path);
|
|
218
|
+
if (payload.target === "terminal") {
|
|
219
|
+
(0, approvals_1.requireApproval)(ctx.config, origin, resolved, "open-terminal", workspaceRoot);
|
|
220
|
+
}
|
|
221
|
+
if (payload.target === "vscode") {
|
|
222
|
+
(0, approvals_1.requireApproval)(ctx.config, origin, resolved, "open-vscode", workspaceRoot);
|
|
223
|
+
}
|
|
224
|
+
await (0, os_1.openTarget)(payload.target, resolved);
|
|
225
|
+
res.json({ ok: true });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
if (err instanceof workspace_1.MissingPathError) {
|
|
229
|
+
next((0, errors_1.pathNotFound)());
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
next(err);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
app.post("/v1/deps/install", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
236
|
+
try {
|
|
237
|
+
const origin = (0, security_1.getOrigin)(req);
|
|
238
|
+
const payload = parseBody(validation_1.depsInstallRequestSchema, req.body);
|
|
239
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
240
|
+
const resolved = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, payload.repoPath);
|
|
241
|
+
try {
|
|
242
|
+
await fs_1.promises.access(path_1.default.join(resolved, "package.json"));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw (0, errors_1.repoNotFound)();
|
|
246
|
+
}
|
|
247
|
+
(0, approvals_1.requireApproval)(ctx.config, origin, resolved, "deps/install", workspaceRoot);
|
|
248
|
+
const job = ctx.jobManager.enqueue(async (jobCtx) => {
|
|
249
|
+
await (0, deps_1.installDeps)(jobCtx, workspaceRoot, {
|
|
250
|
+
repoPath: payload.repoPath,
|
|
251
|
+
manager: payload.manager ?? "auto",
|
|
252
|
+
mode: payload.mode ?? "auto",
|
|
253
|
+
safer: payload.safer ?? ctx.config.deps.defaultSafer,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
res.status(202).json({ jobId: job.id });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
if (err instanceof workspace_1.MissingPathError) {
|
|
260
|
+
next((0, errors_1.pathNotFound)());
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
next(err);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
app.get("/v1/diagnostics", (0, security_1.authGuard)(ctx.tokenStore), (req, res, next) => {
|
|
267
|
+
try {
|
|
268
|
+
const summary = {
|
|
269
|
+
configVersion: ctx.config.configVersion,
|
|
270
|
+
server: ctx.config.server,
|
|
271
|
+
originAllowlist: ctx.config.originAllowlist,
|
|
272
|
+
workspaceRoot: ctx.config.workspaceRoot,
|
|
273
|
+
pairing: ctx.config.pairing,
|
|
274
|
+
jobs: ctx.config.jobs,
|
|
275
|
+
deps: ctx.config.deps,
|
|
276
|
+
logging: ctx.config.logging,
|
|
277
|
+
};
|
|
278
|
+
res.json({
|
|
279
|
+
config: summary,
|
|
280
|
+
recentErrors: [],
|
|
281
|
+
jobs: ctx.jobManager.listRecent(),
|
|
282
|
+
logTail: [],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
next(err);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
app.use((err, _req, res, _next) => {
|
|
290
|
+
if (err instanceof errors_1.ApiError) {
|
|
291
|
+
res.status(err.status).json(err.body);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (err.type === "entity.too.large") {
|
|
295
|
+
res
|
|
296
|
+
.status(413)
|
|
297
|
+
.json((0, errors_1.errorBody)("request_too_large", "Request too large."));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
ctx.logger.error({ err }, "Unhandled error");
|
|
301
|
+
const fallback = (0, errors_1.internalError)();
|
|
302
|
+
res.status(fallback.status).json(fallback.body);
|
|
303
|
+
});
|
|
304
|
+
return app;
|
|
305
|
+
};
|
|
306
|
+
exports.createApp = createApp;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.requireApproval = exports.hasApproval = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const hasApproval = (config, origin, repoPath, capability, workspaceRoot) => config.approvals.entries.some((entry) => {
|
|
10
|
+
if (entry.origin !== origin || !entry.capabilities.includes(capability)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (entry.repoPath === repoPath) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (workspaceRoot && !path_1.default.isAbsolute(entry.repoPath)) {
|
|
17
|
+
return path_1.default.resolve(workspaceRoot, entry.repoPath) === repoPath;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
});
|
|
21
|
+
exports.hasApproval = hasApproval;
|
|
22
|
+
const requireApproval = (config, origin, repoPath, capability, workspaceRoot) => {
|
|
23
|
+
if (!(0, exports.hasApproval)(config, origin, repoPath, capability, workspaceRoot)) {
|
|
24
|
+
throw (0, errors_1.capabilityNotGranted)();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
exports.requireApproval = requireApproval;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.saveConfig = exports.loadConfig = exports.ensureDir = exports.defaultConfig = exports.getTokensPath = exports.getConfigPath = exports.getConfigDir = void 0;
|
|
7
|
+
const env_paths_1 = __importDefault(require("env-paths"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const CONFIG_VERSION = 1;
|
|
11
|
+
const CONFIG_FILE = "config.json";
|
|
12
|
+
const TOKENS_FILE = "tokens.json";
|
|
13
|
+
const getConfigDir = () => {
|
|
14
|
+
const override = process.env.GIT_DAEMON_CONFIG_DIR;
|
|
15
|
+
if (override) {
|
|
16
|
+
return override;
|
|
17
|
+
}
|
|
18
|
+
const paths = (0, env_paths_1.default)("Git Daemon", { suffix: "" });
|
|
19
|
+
return paths.config;
|
|
20
|
+
};
|
|
21
|
+
exports.getConfigDir = getConfigDir;
|
|
22
|
+
const getConfigPath = (configDir) => path_1.default.join(configDir, CONFIG_FILE);
|
|
23
|
+
exports.getConfigPath = getConfigPath;
|
|
24
|
+
const getTokensPath = (configDir) => path_1.default.join(configDir, TOKENS_FILE);
|
|
25
|
+
exports.getTokensPath = getTokensPath;
|
|
26
|
+
const defaultConfig = () => ({
|
|
27
|
+
configVersion: CONFIG_VERSION,
|
|
28
|
+
server: {
|
|
29
|
+
host: "127.0.0.1",
|
|
30
|
+
port: 8790,
|
|
31
|
+
},
|
|
32
|
+
originAllowlist: ["https://app.example.com"],
|
|
33
|
+
workspaceRoot: null,
|
|
34
|
+
pairing: {
|
|
35
|
+
tokenTtlDays: 30,
|
|
36
|
+
},
|
|
37
|
+
jobs: {
|
|
38
|
+
maxConcurrent: 1,
|
|
39
|
+
timeoutSeconds: 3600,
|
|
40
|
+
},
|
|
41
|
+
deps: {
|
|
42
|
+
defaultSafer: true,
|
|
43
|
+
},
|
|
44
|
+
logging: {
|
|
45
|
+
directory: "logs",
|
|
46
|
+
maxFiles: 5,
|
|
47
|
+
maxBytes: 5 * 1024 * 1024,
|
|
48
|
+
},
|
|
49
|
+
approvals: {
|
|
50
|
+
entries: [],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
exports.defaultConfig = defaultConfig;
|
|
54
|
+
const ensureDir = async (dir) => {
|
|
55
|
+
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
56
|
+
};
|
|
57
|
+
exports.ensureDir = ensureDir;
|
|
58
|
+
const loadConfig = async (configDir) => {
|
|
59
|
+
await (0, exports.ensureDir)(configDir);
|
|
60
|
+
const configPath = (0, exports.getConfigPath)(configDir);
|
|
61
|
+
try {
|
|
62
|
+
const raw = await fs_1.promises.readFile(configPath, "utf8");
|
|
63
|
+
const data = JSON.parse(raw);
|
|
64
|
+
return {
|
|
65
|
+
...(0, exports.defaultConfig)(),
|
|
66
|
+
...data,
|
|
67
|
+
server: {
|
|
68
|
+
...(0, exports.defaultConfig)().server,
|
|
69
|
+
...data.server,
|
|
70
|
+
},
|
|
71
|
+
pairing: {
|
|
72
|
+
...(0, exports.defaultConfig)().pairing,
|
|
73
|
+
...data.pairing,
|
|
74
|
+
},
|
|
75
|
+
jobs: {
|
|
76
|
+
...(0, exports.defaultConfig)().jobs,
|
|
77
|
+
...data.jobs,
|
|
78
|
+
},
|
|
79
|
+
deps: {
|
|
80
|
+
...(0, exports.defaultConfig)().deps,
|
|
81
|
+
...data.deps,
|
|
82
|
+
},
|
|
83
|
+
logging: {
|
|
84
|
+
...(0, exports.defaultConfig)().logging,
|
|
85
|
+
...data.logging,
|
|
86
|
+
},
|
|
87
|
+
approvals: {
|
|
88
|
+
entries: data.approvals?.entries ?? [],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err.code !== "ENOENT") {
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const config = (0, exports.defaultConfig)();
|
|
97
|
+
await (0, exports.saveConfig)(configDir, config);
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
exports.loadConfig = loadConfig;
|
|
102
|
+
const saveConfig = async (configDir, config) => {
|
|
103
|
+
const configPath = (0, exports.getConfigPath)(configDir);
|
|
104
|
+
await (0, exports.ensureDir)(configDir);
|
|
105
|
+
await fs_1.promises.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
106
|
+
};
|
|
107
|
+
exports.saveConfig = saveConfig;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createContext = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const logger_1 = require("./logger");
|
|
10
|
+
const tools_1 = require("./tools");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const tokens_1 = require("./tokens");
|
|
13
|
+
const pairing_1 = require("./pairing");
|
|
14
|
+
const jobs_1 = require("./jobs");
|
|
15
|
+
const readPackageVersion = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = path_1.default.resolve(__dirname, "..", "package.json");
|
|
18
|
+
const raw = await fs_1.promises.readFile(pkgPath, "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
return parsed.version ?? "0.0.0";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "0.0.0";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const createContext = async () => {
|
|
27
|
+
const configDir = (0, config_1.getConfigDir)();
|
|
28
|
+
const config = await (0, config_1.loadConfig)(configDir);
|
|
29
|
+
if (!config.originAllowlist.length) {
|
|
30
|
+
throw new Error("originAllowlist must contain at least one entry.");
|
|
31
|
+
}
|
|
32
|
+
if (config.server.host !== "127.0.0.1") {
|
|
33
|
+
throw new Error("Server host must be 127.0.0.1 for loopback-only binding.");
|
|
34
|
+
}
|
|
35
|
+
const tokenStore = new tokens_1.TokenStore(configDir);
|
|
36
|
+
await tokenStore.load();
|
|
37
|
+
const logger = await (0, logger_1.createLogger)(configDir, config.logging);
|
|
38
|
+
const capabilities = await (0, tools_1.detectCapabilities)();
|
|
39
|
+
const pairingManager = new pairing_1.PairingManager(tokenStore, config.pairing.tokenTtlDays);
|
|
40
|
+
const jobManager = new jobs_1.JobManager(config.jobs.maxConcurrent, config.jobs.timeoutSeconds);
|
|
41
|
+
const version = await readPackageVersion();
|
|
42
|
+
const build = {
|
|
43
|
+
commit: process.env.GIT_DAEMON_BUILD_COMMIT,
|
|
44
|
+
date: process.env.GIT_DAEMON_BUILD_DATE,
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
config,
|
|
48
|
+
configDir,
|
|
49
|
+
tokenStore,
|
|
50
|
+
pairingManager,
|
|
51
|
+
jobManager,
|
|
52
|
+
capabilities,
|
|
53
|
+
logger,
|
|
54
|
+
version,
|
|
55
|
+
build,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
exports.createContext = createContext;
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const context_1 = require("./context");
|
|
4
|
+
const app_1 = require("./app");
|
|
5
|
+
const start = async () => {
|
|
6
|
+
const ctx = await (0, context_1.createContext)();
|
|
7
|
+
const app = (0, app_1.createApp)(ctx);
|
|
8
|
+
const startupSummary = {
|
|
9
|
+
configDir: ctx.configDir,
|
|
10
|
+
host: ctx.config.server.host,
|
|
11
|
+
port: ctx.config.server.port,
|
|
12
|
+
workspaceRoot: ctx.config.workspaceRoot ?? "not configured",
|
|
13
|
+
originAllowlist: ctx.config.originAllowlist,
|
|
14
|
+
};
|
|
15
|
+
ctx.logger.info(startupSummary, "Git Daemon starting");
|
|
16
|
+
if (process.env.GIT_DAEMON_LOG_STDOUT !== "1") {
|
|
17
|
+
console.log(`[Git Daemon] config=${startupSummary.configDir} host=${startupSummary.host} port=${startupSummary.port}`);
|
|
18
|
+
}
|
|
19
|
+
app.listen(ctx.config.server.port, ctx.config.server.host, () => {
|
|
20
|
+
ctx.logger.info({
|
|
21
|
+
host: ctx.config.server.host,
|
|
22
|
+
port: ctx.config.server.port,
|
|
23
|
+
}, "Git Daemon listening");
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
start().catch((err) => {
|
|
27
|
+
console.error("Failed to start Git Daemon", err);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|