git-daemon 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +1 -1
- package/README.md +33 -6
- package/config.schema.json +1 -1
- package/design.md +3 -3
- package/dist/app.js +326 -0
- package/dist/approvals.js +27 -0
- package/dist/cli-test-clone.js +122 -0
- package/dist/config.js +107 -0
- package/dist/context.js +58 -0
- package/dist/daemon.js +34 -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/setup.js +165 -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 +4 -1
- package/src/app.ts +24 -1
- package/src/cli-test-clone.ts +154 -0
- package/src/config.ts +1 -1
- package/src/daemon.ts +19 -0
- package/src/jobs.ts +9 -3
- package/src/logger.ts +27 -5
- package/src/process.ts +3 -2
- package/src/setup.ts +165 -0
- 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/.eslintrc.cjs
CHANGED
package/README.md
CHANGED
|
@@ -39,7 +39,34 @@ npm install
|
|
|
39
39
|
npm run daemon
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
The daemon listens on `http://127.0.0.1:
|
|
42
|
+
The daemon listens on `http://127.0.0.1:8790` by default.
|
|
43
|
+
|
|
44
|
+
## Setup workspace root
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run setup
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This prompts for an absolute workspace root path and saves it to your config. The prompt reads from the terminal directly (via `/dev/tty` on macOS/Linux) so it still works in many IDE run configurations.
|
|
51
|
+
For development, you can also run `npm run setup:dev`.
|
|
52
|
+
|
|
53
|
+
Non-interactive setup (no TTY):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
GIT_DAEMON_WORKSPACE_ROOT=/absolute/path npm run setup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run setup -- --workspace=/absolute/path
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Verbose logging options:
|
|
66
|
+
|
|
67
|
+
- `GIT_DAEMON_LOG_STDOUT=1` to mirror logs to stdout
|
|
68
|
+
- `GIT_DAEMON_LOG_PRETTY=0` to disable pretty formatting when stdout is enabled
|
|
69
|
+
- `GIT_DAEMON_LOG_LEVEL=debug` to increase verbosity
|
|
43
70
|
|
|
44
71
|
## Pairing flow
|
|
45
72
|
|
|
@@ -51,7 +78,7 @@ Pairing is required before using protected endpoints.
|
|
|
51
78
|
curl -H "Origin: https://app.example.com" \
|
|
52
79
|
-H "Content-Type: application/json" \
|
|
53
80
|
-d '{"step":"start"}' \
|
|
54
|
-
http://127.0.0.1:
|
|
81
|
+
http://127.0.0.1:8790/v1/pair
|
|
55
82
|
```
|
|
56
83
|
|
|
57
84
|
2. Confirm pairing with the code:
|
|
@@ -60,7 +87,7 @@ curl -H "Origin: https://app.example.com" \
|
|
|
60
87
|
curl -H "Origin: https://app.example.com" \
|
|
61
88
|
-H "Content-Type: application/json" \
|
|
62
89
|
-d '{"step":"confirm","code":"<CODE>"}' \
|
|
63
|
-
http://127.0.0.1:
|
|
90
|
+
http://127.0.0.1:8790/v1/pair
|
|
64
91
|
```
|
|
65
92
|
|
|
66
93
|
The response includes `accessToken` to use as `Authorization: Bearer <token>`.
|
|
@@ -71,7 +98,7 @@ Check meta:
|
|
|
71
98
|
|
|
72
99
|
```bash
|
|
73
100
|
curl -H "Origin: https://app.example.com" \
|
|
74
|
-
http://127.0.0.1:
|
|
101
|
+
http://127.0.0.1:8790/v1/meta
|
|
75
102
|
```
|
|
76
103
|
|
|
77
104
|
Clone a repo (job):
|
|
@@ -82,7 +109,7 @@ curl -X POST \
|
|
|
82
109
|
-H "Authorization: Bearer <TOKEN>" \
|
|
83
110
|
-H "Content-Type: application/json" \
|
|
84
111
|
-d '{"repoUrl":"git@github.com:owner/repo.git","destRelative":"owner/repo"}' \
|
|
85
|
-
http://127.0.0.1:
|
|
112
|
+
http://127.0.0.1:8790/v1/git/clone
|
|
86
113
|
```
|
|
87
114
|
|
|
88
115
|
Stream job logs (SSE):
|
|
@@ -91,7 +118,7 @@ Stream job logs (SSE):
|
|
|
91
118
|
curl -N \
|
|
92
119
|
-H "Origin: https://app.example.com" \
|
|
93
120
|
-H "Authorization: Bearer <TOKEN>" \
|
|
94
|
-
http://127.0.0.1:
|
|
121
|
+
http://127.0.0.1:8790/v1/jobs/<JOB_ID>/stream
|
|
95
122
|
```
|
|
96
123
|
|
|
97
124
|
## 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,326 @@
|
|
|
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
|
+
const isTerminalState = (event) => {
|
|
120
|
+
if (!event || typeof event !== "object") {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const record = event;
|
|
124
|
+
return (record.type === "state" &&
|
|
125
|
+
(record.state === "done" ||
|
|
126
|
+
record.state === "error" ||
|
|
127
|
+
record.state === "cancelled"));
|
|
128
|
+
};
|
|
129
|
+
for (const event of job.events) {
|
|
130
|
+
sendEvent(event);
|
|
131
|
+
if (isTerminalState(event)) {
|
|
132
|
+
res.end();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const listener = (event) => {
|
|
137
|
+
sendEvent(event);
|
|
138
|
+
if (isTerminalState(event)) {
|
|
139
|
+
job.emitter.off("event", listener);
|
|
140
|
+
res.end();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
job.emitter.on("event", listener);
|
|
144
|
+
req.on("close", () => {
|
|
145
|
+
job.emitter.off("event", listener);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
app.post("/v1/jobs/:id/cancel", (0, security_1.authGuard)(ctx.tokenStore), (req, res, next) => {
|
|
149
|
+
try {
|
|
150
|
+
const job = ctx.jobManager.get(req.params.id);
|
|
151
|
+
if (!job) {
|
|
152
|
+
throw (0, errors_1.jobNotFound)();
|
|
153
|
+
}
|
|
154
|
+
if (job.state !== "queued" && job.state !== "running") {
|
|
155
|
+
res
|
|
156
|
+
.status(409)
|
|
157
|
+
.json((0, errors_1.errorBody)("internal_error", "Job is not cancellable."));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
ctx.jobManager.cancel(job.id);
|
|
161
|
+
res.json({ accepted: true });
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
next(err);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
app.post("/v1/git/clone", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
168
|
+
try {
|
|
169
|
+
const payload = parseBody(validation_1.gitCloneRequestSchema, req.body, {
|
|
170
|
+
repoUrl: true,
|
|
171
|
+
});
|
|
172
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
173
|
+
(0, workspace_1.ensureRelative)(payload.destRelative);
|
|
174
|
+
const destPath = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, payload.destRelative, true);
|
|
175
|
+
try {
|
|
176
|
+
await fs_1.promises.access(destPath);
|
|
177
|
+
res
|
|
178
|
+
.status(409)
|
|
179
|
+
.json((0, errors_1.errorBody)("internal_error", "Destination already exists."));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err.code !== "ENOENT") {
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
await fs_1.promises.mkdir(path_1.default.dirname(destPath), { recursive: true });
|
|
188
|
+
const job = ctx.jobManager.enqueue(async (jobCtx) => {
|
|
189
|
+
await (0, git_1.cloneRepo)(jobCtx, workspaceRoot, payload.repoUrl, payload.destRelative, payload.options);
|
|
190
|
+
});
|
|
191
|
+
res.status(202).json({ jobId: job.id });
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
next(err);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
app.post("/v1/git/fetch", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
198
|
+
try {
|
|
199
|
+
const payload = parseBody(validation_1.gitFetchRequestSchema, req.body);
|
|
200
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
201
|
+
await (0, git_1.resolveRepoPath)(workspaceRoot, payload.repoPath);
|
|
202
|
+
const job = ctx.jobManager.enqueue(async (jobCtx) => {
|
|
203
|
+
await (0, git_1.fetchRepo)(jobCtx, workspaceRoot, payload.repoPath, payload.remote, payload.prune);
|
|
204
|
+
});
|
|
205
|
+
res.status(202).json({ jobId: job.id });
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (err instanceof git_1.RepoNotFoundError ||
|
|
209
|
+
err instanceof workspace_1.MissingPathError) {
|
|
210
|
+
next((0, errors_1.repoNotFound)());
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
next(err);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
app.get("/v1/git/status", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
217
|
+
try {
|
|
218
|
+
const { repoPath } = parseBody(validation_1.gitStatusQuerySchema, req.query);
|
|
219
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
220
|
+
const status = await (0, git_1.getRepoStatus)(workspaceRoot, repoPath);
|
|
221
|
+
res.json(status);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (err instanceof git_1.RepoNotFoundError ||
|
|
225
|
+
err instanceof workspace_1.MissingPathError) {
|
|
226
|
+
next((0, errors_1.repoNotFound)());
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next(err);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
app.post("/v1/os/open", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
233
|
+
try {
|
|
234
|
+
const origin = (0, security_1.getOrigin)(req);
|
|
235
|
+
const payload = parseBody(validation_1.osOpenRequestSchema, req.body);
|
|
236
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
237
|
+
const resolved = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, payload.path);
|
|
238
|
+
if (payload.target === "terminal") {
|
|
239
|
+
(0, approvals_1.requireApproval)(ctx.config, origin, resolved, "open-terminal", workspaceRoot);
|
|
240
|
+
}
|
|
241
|
+
if (payload.target === "vscode") {
|
|
242
|
+
(0, approvals_1.requireApproval)(ctx.config, origin, resolved, "open-vscode", workspaceRoot);
|
|
243
|
+
}
|
|
244
|
+
await (0, os_1.openTarget)(payload.target, resolved);
|
|
245
|
+
res.json({ ok: true });
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
if (err instanceof workspace_1.MissingPathError) {
|
|
249
|
+
next((0, errors_1.pathNotFound)());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
next(err);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
app.post("/v1/deps/install", (0, security_1.authGuard)(ctx.tokenStore), async (req, res, next) => {
|
|
256
|
+
try {
|
|
257
|
+
const origin = (0, security_1.getOrigin)(req);
|
|
258
|
+
const payload = parseBody(validation_1.depsInstallRequestSchema, req.body);
|
|
259
|
+
const workspaceRoot = (0, workspace_1.ensureWorkspaceRoot)(ctx.config.workspaceRoot);
|
|
260
|
+
const resolved = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, payload.repoPath);
|
|
261
|
+
try {
|
|
262
|
+
await fs_1.promises.access(path_1.default.join(resolved, "package.json"));
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
throw (0, errors_1.repoNotFound)();
|
|
266
|
+
}
|
|
267
|
+
(0, approvals_1.requireApproval)(ctx.config, origin, resolved, "deps/install", workspaceRoot);
|
|
268
|
+
const job = ctx.jobManager.enqueue(async (jobCtx) => {
|
|
269
|
+
await (0, deps_1.installDeps)(jobCtx, workspaceRoot, {
|
|
270
|
+
repoPath: payload.repoPath,
|
|
271
|
+
manager: payload.manager ?? "auto",
|
|
272
|
+
mode: payload.mode ?? "auto",
|
|
273
|
+
safer: payload.safer ?? ctx.config.deps.defaultSafer,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
res.status(202).json({ jobId: job.id });
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (err instanceof workspace_1.MissingPathError) {
|
|
280
|
+
next((0, errors_1.pathNotFound)());
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
next(err);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
app.get("/v1/diagnostics", (0, security_1.authGuard)(ctx.tokenStore), (req, res, next) => {
|
|
287
|
+
try {
|
|
288
|
+
const summary = {
|
|
289
|
+
configVersion: ctx.config.configVersion,
|
|
290
|
+
server: ctx.config.server,
|
|
291
|
+
originAllowlist: ctx.config.originAllowlist,
|
|
292
|
+
workspaceRoot: ctx.config.workspaceRoot,
|
|
293
|
+
pairing: ctx.config.pairing,
|
|
294
|
+
jobs: ctx.config.jobs,
|
|
295
|
+
deps: ctx.config.deps,
|
|
296
|
+
logging: ctx.config.logging,
|
|
297
|
+
};
|
|
298
|
+
res.json({
|
|
299
|
+
config: summary,
|
|
300
|
+
recentErrors: [],
|
|
301
|
+
jobs: ctx.jobManager.listRecent(),
|
|
302
|
+
logTail: [],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
next(err);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
app.use((err, _req, res, _next) => {
|
|
310
|
+
if (err instanceof errors_1.ApiError) {
|
|
311
|
+
res.status(err.status).json(err.body);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (err.type === "entity.too.large") {
|
|
315
|
+
res
|
|
316
|
+
.status(413)
|
|
317
|
+
.json((0, errors_1.errorBody)("request_too_large", "Request too large."));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
ctx.logger.error({ err }, "Unhandled error");
|
|
321
|
+
const fallback = (0, errors_1.internalError)();
|
|
322
|
+
res.status(fallback.status).json(fallback.body);
|
|
323
|
+
});
|
|
324
|
+
return app;
|
|
325
|
+
};
|
|
326
|
+
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;
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
const http_1 = __importDefault(require("http"));
|
|
7
|
+
const getEnv = (key, fallback) => process.env[key] || fallback;
|
|
8
|
+
const ORIGIN = getEnv("ORIGIN", "https://app.example.com");
|
|
9
|
+
const PORT = Number(getEnv("PORT", "8790"));
|
|
10
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
11
|
+
const REPO = getEnv("REPO_URL", "git@github.com:bunnybones1/git-daemon.git");
|
|
12
|
+
const DEST = getEnv("DEST_RELATIVE", "bunnybones1/git-daemon");
|
|
13
|
+
const requestJson = (path, body) => new Promise((resolve, reject) => {
|
|
14
|
+
const payload = JSON.stringify(body);
|
|
15
|
+
const req = http_1.default.request(`${BASE}${path}`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
Origin: ORIGIN,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
21
|
+
},
|
|
22
|
+
}, (res) => {
|
|
23
|
+
let raw = "";
|
|
24
|
+
res.setEncoding("utf8");
|
|
25
|
+
res.on("data", (chunk) => {
|
|
26
|
+
raw += chunk;
|
|
27
|
+
});
|
|
28
|
+
res.on("end", () => {
|
|
29
|
+
try {
|
|
30
|
+
const data = raw ? JSON.parse(raw) : {};
|
|
31
|
+
resolve({ status: res.statusCode || 0, data });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
reject(err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
req.on("error", reject);
|
|
39
|
+
req.write(payload);
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
const requestJsonAuth = (path, token, body) => new Promise((resolve, reject) => {
|
|
43
|
+
const payload = JSON.stringify(body);
|
|
44
|
+
const req = http_1.default.request(`${BASE}${path}`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
Origin: ORIGIN,
|
|
48
|
+
Authorization: `Bearer ${token}`,
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
51
|
+
},
|
|
52
|
+
}, (res) => {
|
|
53
|
+
let raw = "";
|
|
54
|
+
res.setEncoding("utf8");
|
|
55
|
+
res.on("data", (chunk) => {
|
|
56
|
+
raw += chunk;
|
|
57
|
+
});
|
|
58
|
+
res.on("end", () => {
|
|
59
|
+
try {
|
|
60
|
+
const data = raw ? JSON.parse(raw) : {};
|
|
61
|
+
resolve({ status: res.statusCode || 0, data });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
reject(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
req.on("error", reject);
|
|
69
|
+
req.write(payload);
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
const streamEvents = (jobId, token) => new Promise((resolve, reject) => {
|
|
73
|
+
const req = http_1.default.request(`${BASE}/v1/jobs/${jobId}/stream`, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers: {
|
|
76
|
+
Origin: ORIGIN,
|
|
77
|
+
Authorization: `Bearer ${token}`,
|
|
78
|
+
Accept: "text/event-stream",
|
|
79
|
+
},
|
|
80
|
+
}, (res) => {
|
|
81
|
+
res.setEncoding("utf8");
|
|
82
|
+
res.on("data", (chunk) => {
|
|
83
|
+
process.stdout.write(chunk);
|
|
84
|
+
});
|
|
85
|
+
res.on("end", () => {
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
req.on("error", reject);
|
|
90
|
+
req.end();
|
|
91
|
+
});
|
|
92
|
+
const main = async () => {
|
|
93
|
+
const start = await requestJson("/v1/pair", { step: "start" });
|
|
94
|
+
if (start.status !== 200 || !start.data.code) {
|
|
95
|
+
console.error("Pairing start failed", start.data);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const confirm = await requestJson("/v1/pair", {
|
|
99
|
+
step: "confirm",
|
|
100
|
+
code: start.data.code,
|
|
101
|
+
});
|
|
102
|
+
if (confirm.status !== 200 || !confirm.data.accessToken) {
|
|
103
|
+
console.error("Pairing confirm failed", confirm.data);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const token = confirm.data.accessToken;
|
|
107
|
+
const clone = await requestJsonAuth("/v1/git/clone", token, {
|
|
108
|
+
repoUrl: REPO,
|
|
109
|
+
destRelative: DEST,
|
|
110
|
+
});
|
|
111
|
+
if (clone.status !== 202 || !clone.data.jobId) {
|
|
112
|
+
console.error("Clone request failed", clone.data);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const jobId = clone.data.jobId;
|
|
116
|
+
console.log(`jobId=${jobId}`);
|
|
117
|
+
await streamEvents(jobId, token);
|
|
118
|
+
};
|
|
119
|
+
main().catch((err) => {
|
|
120
|
+
console.error("Test clone failed", err);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|