@code-partner/codepipe-hub 0.14.1-dev.420.g57ff103d → 0.14.1-dev.423.ga49d1f61
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/dashboard/dist/assets/{ModelCanvas-WXJ1hqXD.js → ModelCanvas-CKATjkFe.js} +1 -1
- package/dashboard/dist/assets/{index-K3ia3VFf.js → index-D5-hoCRk.js} +72 -72
- package/dashboard/dist/index.html +1 -1
- package/hub/dist/embedded.js +720 -220
- package/hub/dist/index.js +1136 -919
- package/package.json +1 -1
package/hub/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ var PLATFORM_VERSION;
|
|
|
13
13
|
var init_version_generated = __esm({
|
|
14
14
|
"../packages/shared/dist/version.generated.js"() {
|
|
15
15
|
"use strict";
|
|
16
|
-
PLATFORM_VERSION = "0.14.1-dev.
|
|
16
|
+
PLATFORM_VERSION = "0.14.1-dev.423.ga49d1f61";
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -31,8 +31,8 @@ var init_version = __esm({
|
|
|
31
31
|
"../packages/shared/dist/version.js"() {
|
|
32
32
|
"use strict";
|
|
33
33
|
init_version_generated();
|
|
34
|
-
PROTOCOL_VERSION =
|
|
35
|
-
MIN_SUPPORTED_PROTOCOL =
|
|
34
|
+
PROTOCOL_VERSION = 5;
|
|
35
|
+
MIN_SUPPORTED_PROTOCOL = 5;
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -2960,21 +2960,14 @@ var init_duration = __esm({
|
|
|
2960
2960
|
});
|
|
2961
2961
|
|
|
2962
2962
|
// ../packages/shared/dist/node-handshake.js
|
|
2963
|
-
function
|
|
2964
|
-
const found = [];
|
|
2963
|
+
function readNodeSocketSecret(headers) {
|
|
2965
2964
|
const raw = headers["authorization"];
|
|
2966
2965
|
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
2967
|
-
if (typeof header
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
}
|
|
2973
|
-
const fromQuery = query?.token;
|
|
2974
|
-
if (typeof fromQuery === "string" && fromQuery !== "") {
|
|
2975
|
-
found.push({ token: fromQuery, source: "query" });
|
|
2976
|
-
}
|
|
2977
|
-
return found;
|
|
2966
|
+
if (typeof header !== "string")
|
|
2967
|
+
return null;
|
|
2968
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
2969
|
+
const token = match?.[1]?.trim();
|
|
2970
|
+
return token !== void 0 && token !== "" ? token : null;
|
|
2978
2971
|
}
|
|
2979
2972
|
function decideNodeHandshake(frame, hub, recognizedCapabilities, authenticatedAs) {
|
|
2980
2973
|
const handshake = parseNodeHandshake(frame);
|
|
@@ -8716,7 +8709,12 @@ var init_route_definition = __esm({
|
|
|
8716
8709
|
"bootstrap.request",
|
|
8717
8710
|
"bootstrap.confirm",
|
|
8718
8711
|
"bootstrap.authorizeEmail",
|
|
8719
|
-
"bootstrap.authorizeConfirm"
|
|
8712
|
+
"bootstrap.authorizeConfirm",
|
|
8713
|
+
// DEV-927: a node registers with a registration token in the body — the
|
|
8714
|
+
// token IS the credential, and there is no node identity yet to bear it.
|
|
8715
|
+
"node.sandbox.register",
|
|
8716
|
+
"node.farm.register",
|
|
8717
|
+
"node.runner.register"
|
|
8720
8718
|
];
|
|
8721
8719
|
}
|
|
8722
8720
|
});
|
|
@@ -10580,6 +10578,15 @@ function humanSubjectOf(actor) {
|
|
|
10580
10578
|
return actor.principal.delegatedBy ?? null;
|
|
10581
10579
|
return null;
|
|
10582
10580
|
}
|
|
10581
|
+
function accountOf(actor) {
|
|
10582
|
+
const subjectId = humanSubjectOf(actor);
|
|
10583
|
+
if (subjectId === null)
|
|
10584
|
+
return null;
|
|
10585
|
+
return {
|
|
10586
|
+
subjectId,
|
|
10587
|
+
email: actor.principal.kind === "human" && actor.displayEmail !== void 0 ? actor.displayEmail : null
|
|
10588
|
+
};
|
|
10589
|
+
}
|
|
10583
10590
|
function serviceOf(actor) {
|
|
10584
10591
|
return actor.principal.kind === "service" ? actor.principal.service : null;
|
|
10585
10592
|
}
|
|
@@ -11488,16 +11495,40 @@ function createReadOperations(port, authorizer) {
|
|
|
11488
11495
|
return { projectId, items: rows, revision: valueRevision(rows) };
|
|
11489
11496
|
}
|
|
11490
11497
|
function projectVisibilityOf(actor) {
|
|
11491
|
-
const
|
|
11492
|
-
if (
|
|
11498
|
+
const account = accountOf(actor);
|
|
11499
|
+
if (account === null)
|
|
11493
11500
|
return null;
|
|
11494
11501
|
return {
|
|
11495
|
-
subjectId,
|
|
11496
|
-
...
|
|
11502
|
+
subjectId: account.subjectId,
|
|
11503
|
+
...account.email === null ? {} : { email: account.email },
|
|
11497
11504
|
...actor.projectScope === void 0 ? {} : { projectScope: actor.projectScope },
|
|
11498
11505
|
includeUnowned: actor.authMethod !== "cli_token"
|
|
11499
11506
|
};
|
|
11500
11507
|
}
|
|
11508
|
+
function farmsView(rows) {
|
|
11509
|
+
return {
|
|
11510
|
+
items: rows.map((r) => ({
|
|
11511
|
+
id: r.farmId,
|
|
11512
|
+
name: r.name,
|
|
11513
|
+
publicKey: r.publicKey,
|
|
11514
|
+
keyFingerprint: r.keyFingerprint,
|
|
11515
|
+
online: r.online,
|
|
11516
|
+
lastSeenAt: toOptionalTimestamp(r.lastSeenAt)
|
|
11517
|
+
}))
|
|
11518
|
+
};
|
|
11519
|
+
}
|
|
11520
|
+
function projectDeliverablesView(projectId, rows) {
|
|
11521
|
+
return {
|
|
11522
|
+
projectId,
|
|
11523
|
+
items: rows.map((r) => ({
|
|
11524
|
+
taskKey: r.taskKey,
|
|
11525
|
+
title: r.title,
|
|
11526
|
+
phase: r.phase,
|
|
11527
|
+
updatedAt: toTimestamp(r.updatedAt),
|
|
11528
|
+
repos: r.repos.map((x) => ({ repoName: x.repoName, branchName: x.branchName, baseRef: x.baseRef }))
|
|
11529
|
+
}))
|
|
11530
|
+
};
|
|
11531
|
+
}
|
|
11501
11532
|
async function visibleProjects(actor) {
|
|
11502
11533
|
const visibility = projectVisibilityOf(actor);
|
|
11503
11534
|
return visibility === null ? [] : await port.visibleProjectIds(visibility);
|
|
@@ -11966,6 +11997,15 @@ function createReadOperations(port, authorizer) {
|
|
|
11966
11997
|
const rows = await port.listRepos(query.projectId);
|
|
11967
11998
|
return reposView(query.projectId, rows);
|
|
11968
11999
|
},
|
|
12000
|
+
async listProjectDeliverables(actor, query) {
|
|
12001
|
+
await require2(actor, "delivery.read", { type: "deliverable", projectId: query.projectId });
|
|
12002
|
+
return projectDeliverablesView(query.projectId, await port.listProjectDeliverables(query.projectId));
|
|
12003
|
+
},
|
|
12004
|
+
async listFarms(actor) {
|
|
12005
|
+
await require2(actor, "account.read", { type: "account" });
|
|
12006
|
+
const account = accountOf(actor);
|
|
12007
|
+
return farmsView(account === null ? [] : await port.listFarms(account));
|
|
12008
|
+
},
|
|
11969
12009
|
async listConfigProposals(actor, query) {
|
|
11970
12010
|
await require2(actor, "project.read", { type: "project", id: query.projectId, projectId: query.projectId });
|
|
11971
12011
|
const limit = normalizeLimit(query.limit);
|
|
@@ -12195,8 +12235,18 @@ function createCommandOperations(deps) {
|
|
|
12195
12235
|
});
|
|
12196
12236
|
case "unprocessable":
|
|
12197
12237
|
throw errors.unprocessable({ details: { reason: outcome.reason } });
|
|
12238
|
+
case "not_found":
|
|
12239
|
+
throw errors.notFound();
|
|
12240
|
+
case "rate_limited":
|
|
12241
|
+
throw errors.rateLimited();
|
|
12198
12242
|
}
|
|
12199
12243
|
}
|
|
12244
|
+
function accountActorOf(actor) {
|
|
12245
|
+
const account = accountOf(actor);
|
|
12246
|
+
if (account === null)
|
|
12247
|
+
throw errors.forbidden({ details: { reason: "human_only" } });
|
|
12248
|
+
return account;
|
|
12249
|
+
}
|
|
12200
12250
|
async function audited(actor, entry, run) {
|
|
12201
12251
|
const policy = { decision: "not_evaluated" };
|
|
12202
12252
|
const common = {
|
|
@@ -12685,6 +12735,115 @@ function createCommandOperations(deps) {
|
|
|
12685
12735
|
return { value: outcome.result, replayed: outcome.replayed };
|
|
12686
12736
|
});
|
|
12687
12737
|
},
|
|
12738
|
+
async createProject(actor, command) {
|
|
12739
|
+
const { project } = command;
|
|
12740
|
+
const projectId = project.projectId;
|
|
12741
|
+
if (!PROJECT_ID_RE.test(projectId)) {
|
|
12742
|
+
throw errors.invalidInput({
|
|
12743
|
+
message: "A project id is lowercase letters, digits and dashes, starts with a letter, 2\u201331 characters.",
|
|
12744
|
+
details: { field: "projectId" }
|
|
12745
|
+
});
|
|
12746
|
+
}
|
|
12747
|
+
for (const repo of project.repos) {
|
|
12748
|
+
if (repo.defaultBranch !== void 0 && !isValidBranchName(repo.defaultBranch)) {
|
|
12749
|
+
throw errors.invalidInput({ message: "That is not a valid git branch name.", details: { field: "repos.defaultBranch", repo: repo.name } });
|
|
12750
|
+
}
|
|
12751
|
+
}
|
|
12752
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12753
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "project.create", projectId, key };
|
|
12754
|
+
return await audited(actor, { operationId: "project.create", aggregateType: "project", aggregateId: projectId, projectId, data: { name: project.name, tracker: project.trackerProvider } }, async (policy) => {
|
|
12755
|
+
await require2(actor, "account.manage", { type: "account" }, policy);
|
|
12756
|
+
const run = await runIdempotent(idempotency, scope, { project }, async () => unwrap(await write2.createProject({ project, owner: accountActorOf(actor) })), now());
|
|
12757
|
+
return { value: run.result, replayed: run.replayed };
|
|
12758
|
+
});
|
|
12759
|
+
},
|
|
12760
|
+
async provisionOnFarm(actor, command) {
|
|
12761
|
+
const { farmId, projectId } = command;
|
|
12762
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12763
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "farm.provision", projectId, key };
|
|
12764
|
+
return await audited(actor, { operationId: "farm.provision", aggregateType: "project", aggregateId: projectId, projectId, data: { farmId } }, async (policy) => {
|
|
12765
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12766
|
+
const run = await runIdempotent(idempotency, scope, { farmId, projectId }, async () => {
|
|
12767
|
+
const row = unwrap(await write2.provisionOnFarm({ farmId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
|
|
12768
|
+
return { projectId, farmId, ok: row.ok, daemonStatus: row.daemonStatus, error: row.error };
|
|
12769
|
+
}, now());
|
|
12770
|
+
return { value: run.result, replayed: run.replayed };
|
|
12771
|
+
});
|
|
12772
|
+
},
|
|
12773
|
+
async provisionGhRunners(actor, command) {
|
|
12774
|
+
const { runnerId, projectId } = command;
|
|
12775
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12776
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "runner.provisionGhRunners", projectId, key };
|
|
12777
|
+
return await audited(actor, { operationId: "runner.provisionGhRunners", aggregateType: "project", aggregateId: projectId, projectId, data: { runnerId } }, async (policy) => {
|
|
12778
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12779
|
+
const run = await runIdempotent(idempotency, scope, { runnerId, projectId }, async () => {
|
|
12780
|
+
const row = unwrap(await write2.provisionGhRunners({ runnerId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
|
|
12781
|
+
return {
|
|
12782
|
+
projectId,
|
|
12783
|
+
runnerId,
|
|
12784
|
+
ok: row.ok,
|
|
12785
|
+
runners: row.runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error, labels: r.labels })),
|
|
12786
|
+
error: row.error
|
|
12787
|
+
};
|
|
12788
|
+
}, now());
|
|
12789
|
+
return { value: run.result, replayed: run.replayed };
|
|
12790
|
+
});
|
|
12791
|
+
},
|
|
12792
|
+
async teardownGhRunners(actor, command) {
|
|
12793
|
+
const { runnerId, projectId } = command;
|
|
12794
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12795
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "runner.teardownGhRunners", projectId, key };
|
|
12796
|
+
const repo = command.repo ?? null;
|
|
12797
|
+
return await audited(actor, {
|
|
12798
|
+
operationId: "runner.teardownGhRunners",
|
|
12799
|
+
aggregateType: "project",
|
|
12800
|
+
aggregateId: projectId,
|
|
12801
|
+
projectId,
|
|
12802
|
+
data: { runnerId, ...repo === null ? {} : { repo: `${repo.owner}/${repo.repo}` } }
|
|
12803
|
+
}, async (policy) => {
|
|
12804
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12805
|
+
const run = await runIdempotent(idempotency, scope, { runnerId, projectId, repo }, async () => {
|
|
12806
|
+
const row = unwrap(await write2.teardownGhRunners({
|
|
12807
|
+
runnerId,
|
|
12808
|
+
projectId,
|
|
12809
|
+
sealedBundle: command.sealedBundle ?? null,
|
|
12810
|
+
repo,
|
|
12811
|
+
actor: accountActorOf(actor)
|
|
12812
|
+
}));
|
|
12813
|
+
return { projectId, runnerId, ok: row.ok, error: row.error };
|
|
12814
|
+
}, now());
|
|
12815
|
+
return { value: run.result, replayed: run.replayed };
|
|
12816
|
+
});
|
|
12817
|
+
},
|
|
12818
|
+
async requestProjectIndex(actor, command) {
|
|
12819
|
+
const { projectId, kind } = command;
|
|
12820
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12821
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "project.index", projectId, key };
|
|
12822
|
+
return await audited(actor, { operationId: "project.index", aggregateType: "project", aggregateId: projectId, projectId, data: { kind } }, async (policy) => {
|
|
12823
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12824
|
+
const run = await runIdempotent(idempotency, scope, { kind }, async () => {
|
|
12825
|
+
const { jobId } = unwrap(await write2.enqueueProjectIndex({ projectId, kind }));
|
|
12826
|
+
return { jobId, kind };
|
|
12827
|
+
}, now());
|
|
12828
|
+
return { value: run.result, replayed: run.replayed };
|
|
12829
|
+
});
|
|
12830
|
+
},
|
|
12831
|
+
async issueWorkerRegistrationToken(actor) {
|
|
12832
|
+
const subjectId = actor.principal.subjectId;
|
|
12833
|
+
return await audited(actor, { operationId: "worker.issueRegistrationToken", aggregateType: "account", aggregateId: subjectId, projectId: null }, async (policy) => {
|
|
12834
|
+
await require2(actor, "account.manage", { type: "account" }, policy);
|
|
12835
|
+
const issued = unwrap(await write2.issueWorkerRegistrationToken({ owner: accountActorOf(actor) }));
|
|
12836
|
+
return { value: { secret: issued.secret, expiresAt: toTimestamp(issued.expiresAt) }, replayed: false };
|
|
12837
|
+
});
|
|
12838
|
+
},
|
|
12839
|
+
async reportInitStatus(actor, command) {
|
|
12840
|
+
const { projectId, phase } = command;
|
|
12841
|
+
return await audited(actor, { operationId: "project.reportInitStatus", aggregateType: "project", aggregateId: projectId, projectId, data: { phase, failed: command.error !== void 0 } }, async (policy) => {
|
|
12842
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12843
|
+
const { recorded } = unwrap(await write2.reportInitStatus({ projectId, phase, error: command.error ?? null }));
|
|
12844
|
+
return { value: { recorded }, replayed: false };
|
|
12845
|
+
});
|
|
12846
|
+
},
|
|
12688
12847
|
async setRepoDefaultBranch(actor, command) {
|
|
12689
12848
|
const { projectId, name } = command;
|
|
12690
12849
|
const revision = asRevision(command.revision);
|
|
@@ -12974,8 +13133,8 @@ var init_cutover_preflight = __esm({
|
|
|
12974
13133
|
SECRET_CONSUMERS = [
|
|
12975
13134
|
{ consumer: "agent credential in the job bundle (Claude / Codex)", purpose: "agent_execution", implemented: true, note: "issued per job from the project's auth mode" },
|
|
12976
13135
|
{ consumer: "repository clone tokens in the job bundle (GitHub / GitLab / Bitbucket)", purpose: "repository_read", implemented: true, note: "one grant for each host the job clones" },
|
|
12977
|
-
{ consumer: "Context Repo push of the delivery from the SANDBOX (contextWrite)", purpose: "context_write", implemented:
|
|
12978
|
-
{ consumer: "mirror clone token in the job bundle (mirrorWrite, read use)", purpose: "repository_read", implemented:
|
|
13136
|
+
{ consumer: "Context Repo push of the delivery from the SANDBOX (contextWrite)", purpose: "context_write", implemented: true, note: "the SANDBOX publishes the delivery over the host-local relay and the holder of the materializer role pushes it; no write credential on the SANDBOX" },
|
|
13137
|
+
{ consumer: "mirror clone token in the job bundle (mirrorWrite, read use)", purpose: "repository_read", implemented: true, note: "a read grant under the mirror's host, issued by the holder of the role with the other repository grants" },
|
|
12979
13138
|
{ consumer: "tracker writes (YouTrack) \u2014 CLI-side, never on the SANDBOX", purpose: "tracker_write", implemented: true, note: "nothing to move: the holder of the role already writes" },
|
|
12980
13139
|
{ consumer: "named project secrets in custom-action runs \u2014 CLI-side", purpose: "custom_action", implemented: true, note: "nothing to move until a tool-run executes on the SANDBOX" }
|
|
12981
13140
|
];
|
|
@@ -25654,10 +25813,16 @@ async function deleteSandboxRegistration(db, sandboxId) {
|
|
|
25654
25813
|
async function sandboxRoutes(app, deps) {
|
|
25655
25814
|
const { db, config } = deps;
|
|
25656
25815
|
const registerByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 30 });
|
|
25657
|
-
const regTokenMintByEmail = createAbuseThrottle({ windowMs: 60 * 6e4, max: 30 });
|
|
25658
25816
|
app.post(
|
|
25659
|
-
"/sandbox/register",
|
|
25817
|
+
"/node/v1/sandbox/register",
|
|
25660
25818
|
{
|
|
25819
|
+
...routeMeta({
|
|
25820
|
+
surface: "node",
|
|
25821
|
+
operationId: "node.sandbox.register",
|
|
25822
|
+
authPolicy: "public",
|
|
25823
|
+
stability: "preview",
|
|
25824
|
+
summary: "Register a SANDBOX with a registration token"
|
|
25825
|
+
}),
|
|
25661
25826
|
schema: {
|
|
25662
25827
|
body: {
|
|
25663
25828
|
type: "object",
|
|
@@ -25698,14 +25863,29 @@ async function sandboxRoutes(app, deps) {
|
|
|
25698
25863
|
return { sandboxId: id, token };
|
|
25699
25864
|
}
|
|
25700
25865
|
);
|
|
25701
|
-
app.get("/sandbox/me",
|
|
25866
|
+
app.get("/node/v1/sandbox/me", routeMeta({
|
|
25867
|
+
surface: "node",
|
|
25868
|
+
operationId: "node.sandbox.me",
|
|
25869
|
+
authPolicy: "node_actor",
|
|
25870
|
+
resourcePolicy: "node.connect",
|
|
25871
|
+
stability: "preview",
|
|
25872
|
+
summary: "Is this SANDBOX registration still known"
|
|
25873
|
+
}), async (req, reply) => {
|
|
25702
25874
|
const sandbox = await authenticateSandboxByBearer(db, req.headers.authorization);
|
|
25703
25875
|
if (!sandbox) return reply.status(401).send({ error: "unauthorized" });
|
|
25704
25876
|
return { sandboxId: sandbox.id, name: sandbox.name };
|
|
25705
25877
|
});
|
|
25706
25878
|
app.post(
|
|
25707
|
-
"/sandbox/agent-auth",
|
|
25879
|
+
"/node/v1/sandbox/agent-auth",
|
|
25708
25880
|
{
|
|
25881
|
+
...routeMeta({
|
|
25882
|
+
surface: "node",
|
|
25883
|
+
operationId: "node.sandbox.agentAuth",
|
|
25884
|
+
authPolicy: "node_actor",
|
|
25885
|
+
resourcePolicy: "node.connect",
|
|
25886
|
+
stability: "preview",
|
|
25887
|
+
summary: "Report the host's agent login and ask whether to probe it"
|
|
25888
|
+
}),
|
|
25709
25889
|
schema: {
|
|
25710
25890
|
body: {
|
|
25711
25891
|
type: "object",
|
|
@@ -25741,8 +25921,16 @@ async function sandboxRoutes(app, deps) {
|
|
|
25741
25921
|
}
|
|
25742
25922
|
);
|
|
25743
25923
|
app.post(
|
|
25744
|
-
"/sandbox/agent-auth/result",
|
|
25924
|
+
"/node/v1/sandbox/agent-auth/result",
|
|
25745
25925
|
{
|
|
25926
|
+
...routeMeta({
|
|
25927
|
+
surface: "node",
|
|
25928
|
+
operationId: "node.sandbox.agentAuthResult",
|
|
25929
|
+
authPolicy: "node_actor",
|
|
25930
|
+
resourcePolicy: "node.connect",
|
|
25931
|
+
stability: "preview",
|
|
25932
|
+
summary: "Report the verdict of an agent login probe"
|
|
25933
|
+
}),
|
|
25746
25934
|
schema: {
|
|
25747
25935
|
body: {
|
|
25748
25936
|
type: "object",
|
|
@@ -25792,19 +25980,6 @@ async function sandboxRoutes(app, deps) {
|
|
|
25792
25980
|
app.log.info({ email: session.email, revoked }, "sandbox reg token revoked");
|
|
25793
25981
|
return { revoked };
|
|
25794
25982
|
});
|
|
25795
|
-
app.post("/sandbox/reg-token/cli", async (req, reply) => {
|
|
25796
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
25797
|
-
if (!auth) {
|
|
25798
|
-
return reply.status(401).send({ error: "bad_cli_token" });
|
|
25799
|
-
}
|
|
25800
|
-
if (!regTokenMintByEmail.take(`email:${auth.email}`)) {
|
|
25801
|
-
app.log.warn({ email: auth.email }, "ephemeral sandbox reg token mint throttled");
|
|
25802
|
-
return reply.status(429).send({ error: "too_many_requests" });
|
|
25803
|
-
}
|
|
25804
|
-
const { secret, expiresAt } = await issueEphemeralSandboxRegToken(db, auth.email);
|
|
25805
|
-
app.log.info({ email: auth.email }, "ephemeral sandbox reg token issued for CLI");
|
|
25806
|
-
return { secret, expiresAt };
|
|
25807
|
-
});
|
|
25808
25983
|
app.delete(
|
|
25809
25984
|
"/sandbox/:sandboxId",
|
|
25810
25985
|
async (req, reply) => {
|
|
@@ -25841,163 +26016,16 @@ var init_sandbox2 = __esm({
|
|
|
25841
26016
|
init_sealedbox();
|
|
25842
26017
|
init_abuse_throttle();
|
|
25843
26018
|
init_auth();
|
|
25844
|
-
init_cli_auth();
|
|
25845
26019
|
init_sandbox_reg_tokens();
|
|
25846
26020
|
init_users();
|
|
25847
26021
|
init_agent_auth_probe();
|
|
25848
26022
|
init_ws();
|
|
25849
|
-
|
|
25850
|
-
});
|
|
25851
|
-
|
|
25852
|
-
// src/farm/pool-spec.ts
|
|
25853
|
-
var pool_spec_exports = {};
|
|
25854
|
-
__export(pool_spec_exports, {
|
|
25855
|
-
buildSyncWorkerPoolMessage: () => buildSyncWorkerPoolMessage,
|
|
25856
|
-
computeFarmPoolSpec: () => computeFarmPoolSpec,
|
|
25857
|
-
reconcileFarmPools: () => reconcileFarmPools
|
|
25858
|
-
});
|
|
25859
|
-
import { nanoid as nanoid25 } from "nanoid";
|
|
25860
|
-
async function computeFarmPoolSpec(db, farmId) {
|
|
25861
|
-
const rows = await db.all(
|
|
25862
|
-
`SELECT fa.user_id as userId, fa.max_sandboxes as quota,
|
|
25863
|
-
p.id as projectId
|
|
25864
|
-
FROM farm_assignments fa
|
|
25865
|
-
JOIN projects p ON p.farm_id = fa.farm_id AND p.owner_user_id = fa.user_id
|
|
25866
|
-
WHERE fa.farm_id = ?
|
|
25867
|
-
ORDER BY fa.assigned_at ASC, p.id ASC`,
|
|
25868
|
-
farmId
|
|
25869
|
-
);
|
|
25870
|
-
const ceilingRow = await db.get(
|
|
25871
|
-
`SELECT max_sandboxes as ceiling FROM farms WHERE id = ?`,
|
|
25872
|
-
farmId
|
|
25873
|
-
);
|
|
25874
|
-
const ceiling = ceilingRow ? Number(ceilingRow.ceiling) : 16;
|
|
25875
|
-
const byAccount = /* @__PURE__ */ new Map();
|
|
25876
|
-
for (const r of rows) {
|
|
25877
|
-
const acc = byAccount.get(r.userId) ?? { quota: Number(r.quota), projectIds: [] };
|
|
25878
|
-
acc.projectIds.push(r.projectId);
|
|
25879
|
-
byAccount.set(r.userId, acc);
|
|
25880
|
-
}
|
|
25881
|
-
const spec = [];
|
|
25882
|
-
let remaining = ceiling;
|
|
25883
|
-
for (const [userId, acc] of byAccount) {
|
|
25884
|
-
const quota = Math.max(0, Math.min(acc.quota, remaining));
|
|
25885
|
-
remaining -= quota;
|
|
25886
|
-
spec.push({ userId, projectIds: acc.projectIds, quota });
|
|
25887
|
-
}
|
|
25888
|
-
return spec;
|
|
25889
|
-
}
|
|
25890
|
-
function buildSyncWorkerPoolMessage(accounts) {
|
|
25891
|
-
return { type: "sync_worker_pool", requestId: nanoid25(16), accounts };
|
|
25892
|
-
}
|
|
25893
|
-
async function reconcileFarmPools(db, send, log) {
|
|
25894
|
-
const farms = await db.all(`SELECT id FROM farms`);
|
|
25895
|
-
const live = new Set(farms.map((f) => f.id));
|
|
25896
|
-
for (const id of [...starvationReported]) {
|
|
25897
|
-
if (!live.has(id)) starvationReported.delete(id);
|
|
25898
|
-
}
|
|
25899
|
-
const starved = [];
|
|
25900
|
-
let pushed = 0;
|
|
25901
|
-
for (const farm of farms) {
|
|
25902
|
-
const accounts = await computeFarmPoolSpec(db, farm.id);
|
|
25903
|
-
if (accounts.length > 0) {
|
|
25904
|
-
const sent = send(farm.id, buildSyncWorkerPoolMessage(accounts));
|
|
25905
|
-
if (sent !== false) pushed++;
|
|
25906
|
-
starvationReported.delete(farm.id);
|
|
25907
|
-
continue;
|
|
25908
|
-
}
|
|
25909
|
-
const queued = await db.get(
|
|
25910
|
-
`SELECT COUNT(*) AS n FROM jobs j
|
|
25911
|
-
JOIN projects p ON p.id = j.project_id
|
|
25912
|
-
WHERE j.state = 'queued' AND p.farm_id = ?`,
|
|
25913
|
-
farm.id
|
|
25914
|
-
);
|
|
25915
|
-
if (Number(queued.n) === 0) {
|
|
25916
|
-
starvationReported.delete(farm.id);
|
|
25917
|
-
continue;
|
|
25918
|
-
}
|
|
25919
|
-
starved.push(farm.id);
|
|
25920
|
-
if (!starvationReported.has(farm.id)) {
|
|
25921
|
-
starvationReported.add(farm.id);
|
|
25922
|
-
log.warn(
|
|
25923
|
-
{ farmId: farm.id, queued: Number(queued.n) },
|
|
25924
|
-
"farm has NO worker-pool accounts while jobs are queued \u2014 assignments missing?"
|
|
25925
|
-
);
|
|
25926
|
-
}
|
|
25927
|
-
}
|
|
25928
|
-
return { farms: farms.length, pushed, starved };
|
|
25929
|
-
}
|
|
25930
|
-
var starvationReported;
|
|
25931
|
-
var init_pool_spec = __esm({
|
|
25932
|
-
"src/farm/pool-spec.ts"() {
|
|
25933
|
-
"use strict";
|
|
25934
|
-
starvationReported = /* @__PURE__ */ new Set();
|
|
25935
|
-
}
|
|
25936
|
-
});
|
|
25937
|
-
|
|
25938
|
-
// src/farm/provision.ts
|
|
25939
|
-
import { nanoid as nanoid26 } from "nanoid";
|
|
25940
|
-
function handleFarmResult(msg) {
|
|
25941
|
-
const type = msg["type"];
|
|
25942
|
-
if (typeof type !== "string" || !RESULT_TYPES.has(type)) return;
|
|
25943
|
-
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
25944
|
-
const p = pending2.get(requestId);
|
|
25945
|
-
if (!p) return;
|
|
25946
|
-
clearTimeout(p.timer);
|
|
25947
|
-
pending2.delete(requestId);
|
|
25948
|
-
p.resolve(msg);
|
|
25949
|
-
}
|
|
25950
|
-
function awaitResult(requestId, send, timeoutMs) {
|
|
25951
|
-
if (!send()) return Promise.reject(new Error("farm_offline"));
|
|
25952
|
-
return new Promise((resolve4, reject) => {
|
|
25953
|
-
const timer = setTimeout(() => {
|
|
25954
|
-
pending2.delete(requestId);
|
|
25955
|
-
reject(new Error("farm_timeout"));
|
|
25956
|
-
}, timeoutMs);
|
|
25957
|
-
pending2.set(requestId, { resolve: resolve4, reject, timer });
|
|
25958
|
-
});
|
|
25959
|
-
}
|
|
25960
|
-
function requestProvision(farmId, projectId, sealedBundle, timeoutMs) {
|
|
25961
|
-
const requestId = nanoid26(16);
|
|
25962
|
-
return awaitResult(
|
|
25963
|
-
requestId,
|
|
25964
|
-
() => sendToFarm(farmId, { type: "provision_project", requestId, projectId, sealedBundle }),
|
|
25965
|
-
timeoutMs
|
|
25966
|
-
);
|
|
25967
|
-
}
|
|
25968
|
-
function requestDelete(farmId, projectId, timeoutMs) {
|
|
25969
|
-
const requestId = nanoid26(16);
|
|
25970
|
-
return awaitResult(
|
|
25971
|
-
requestId,
|
|
25972
|
-
() => sendToFarm(farmId, { type: "delete_project", requestId, projectId }),
|
|
25973
|
-
timeoutMs
|
|
25974
|
-
);
|
|
25975
|
-
}
|
|
25976
|
-
function requestRespawn(farmId, projectId, timeoutMs) {
|
|
25977
|
-
const requestId = nanoid26(16);
|
|
25978
|
-
return awaitResult(
|
|
25979
|
-
requestId,
|
|
25980
|
-
() => sendToFarm(farmId, { type: "respawn_project", requestId, projectId }),
|
|
25981
|
-
timeoutMs
|
|
25982
|
-
);
|
|
25983
|
-
}
|
|
25984
|
-
async function requestFarmPoolSync(db, farmId) {
|
|
25985
|
-
const accounts = await computeFarmPoolSpec(db, farmId);
|
|
25986
|
-
sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
|
|
25987
|
-
}
|
|
25988
|
-
var pending2, RESULT_TYPES;
|
|
25989
|
-
var init_provision = __esm({
|
|
25990
|
-
"src/farm/provision.ts"() {
|
|
25991
|
-
"use strict";
|
|
25992
|
-
init_ws();
|
|
25993
|
-
init_pool_spec();
|
|
25994
|
-
pending2 = /* @__PURE__ */ new Map();
|
|
25995
|
-
RESULT_TYPES = /* @__PURE__ */ new Set(["provision_result", "delete_result", "respawn_result"]);
|
|
26023
|
+
init_route_registry();
|
|
25996
26024
|
}
|
|
25997
26025
|
});
|
|
25998
26026
|
|
|
25999
26027
|
// src/routes/farm.ts
|
|
26000
|
-
import { nanoid as
|
|
26028
|
+
import { nanoid as nanoid25 } from "nanoid";
|
|
26001
26029
|
function viewerInAssignments(viewer, assignments) {
|
|
26002
26030
|
return assignments.some(
|
|
26003
26031
|
(a) => viewer.userId !== null && a.userId === viewer.userId || a.email !== null && a.email === viewer.email
|
|
@@ -26056,8 +26084,15 @@ async function farmRoutes(app, deps) {
|
|
|
26056
26084
|
const { db, config } = deps;
|
|
26057
26085
|
const registerByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 10 });
|
|
26058
26086
|
app.post(
|
|
26059
|
-
"/farm/register",
|
|
26087
|
+
"/node/v1/farm/register",
|
|
26060
26088
|
{
|
|
26089
|
+
...routeMeta({
|
|
26090
|
+
surface: "node",
|
|
26091
|
+
operationId: "node.farm.register",
|
|
26092
|
+
authPolicy: "public",
|
|
26093
|
+
stability: "preview",
|
|
26094
|
+
summary: "Register a FARM with the operator's registration token"
|
|
26095
|
+
}),
|
|
26061
26096
|
schema: {
|
|
26062
26097
|
body: {
|
|
26063
26098
|
type: "object",
|
|
@@ -26082,7 +26117,7 @@ async function farmRoutes(app, deps) {
|
|
|
26082
26117
|
if (req.body.registrationToken !== config.farmRegistrationToken) {
|
|
26083
26118
|
return reply.status(401).send({ error: "bad_registration_token" });
|
|
26084
26119
|
}
|
|
26085
|
-
const token =
|
|
26120
|
+
const token = nanoid25(48);
|
|
26086
26121
|
const now = Date.now();
|
|
26087
26122
|
const publicKey = req.body.publicKey ?? null;
|
|
26088
26123
|
const fingerprint2 = publicKey ? publicKeyFingerprint(publicKey) : null;
|
|
@@ -26121,7 +26156,7 @@ async function farmRoutes(app, deps) {
|
|
|
26121
26156
|
"farm registered under an existing name with a DIFFERENT key \u2014 new identity; assignments stay with the old farm and must be moved deliberately"
|
|
26122
26157
|
);
|
|
26123
26158
|
}
|
|
26124
|
-
const id = `farm_${
|
|
26159
|
+
const id = `farm_${nanoid25(16)}`;
|
|
26125
26160
|
await db.run(`INSERT INTO farms
|
|
26126
26161
|
(id, name, token, capabilities_json, public_key, key_fingerprint, registered_at, last_seen_at, max_sandboxes)
|
|
26127
26162
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, req.body.name, token, JSON.stringify(req.body.capabilities), publicKey, fingerprint2, now, now, maxSandboxes);
|
|
@@ -26129,104 +26164,26 @@ async function farmRoutes(app, deps) {
|
|
|
26129
26164
|
return { farmId: id, token };
|
|
26130
26165
|
}
|
|
26131
26166
|
);
|
|
26132
|
-
app.get("/farms", async (req, reply) => {
|
|
26133
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
26134
|
-
if (auth) {
|
|
26135
|
-
return await listFarms(db, {
|
|
26136
|
-
email: auth.email,
|
|
26137
|
-
userId: auth.userId,
|
|
26138
|
-
isSuperAdmin: isSuperUser(config, auth.email)
|
|
26139
|
-
});
|
|
26140
|
-
}
|
|
26141
|
-
const session = await requireSession(db, req, reply);
|
|
26142
|
-
if (!session) return reply;
|
|
26143
|
-
return await listFarms(db, {
|
|
26144
|
-
email: session.email,
|
|
26145
|
-
userId: session.userId,
|
|
26146
|
-
isSuperAdmin: isSuperUser(config, session.email)
|
|
26147
|
-
});
|
|
26148
|
-
});
|
|
26149
|
-
app.post(
|
|
26150
|
-
"/farms/:farmId/provision",
|
|
26151
|
-
{
|
|
26152
|
-
schema: {
|
|
26153
|
-
body: {
|
|
26154
|
-
type: "object",
|
|
26155
|
-
required: ["projectId", "sealedBundle"],
|
|
26156
|
-
properties: {
|
|
26157
|
-
projectId: { type: "string" },
|
|
26158
|
-
sealedBundle: { type: "string", minLength: 1 }
|
|
26159
|
-
}
|
|
26160
|
-
}
|
|
26161
|
-
}
|
|
26162
|
-
},
|
|
26163
|
-
async (req, reply) => {
|
|
26164
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
26165
|
-
if (!auth) {
|
|
26166
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
26167
|
-
}
|
|
26168
|
-
const { farmId } = req.params;
|
|
26169
|
-
const { projectId, sealedBundle } = req.body;
|
|
26170
|
-
if (!await canActOnProject(db, config, auth, projectId)) {
|
|
26171
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
26172
|
-
}
|
|
26173
|
-
const farm = await db.get(`SELECT id FROM farms WHERE id = ?`, farmId);
|
|
26174
|
-
if (!farm) return reply.status(404).send({ error: "unknown_farm" });
|
|
26175
|
-
if (!isSuperUser(config, auth.email) && !await viewerOwnsFarm(db, { email: auth.email, userId: auth.userId }, farmId)) {
|
|
26176
|
-
return reply.status(403).send({ error: "farm_not_assigned" });
|
|
26177
|
-
}
|
|
26178
|
-
if (!await db.get(`SELECT id FROM projects WHERE id = ?`, projectId)) {
|
|
26179
|
-
return reply.status(404).send({ error: "unknown_project" });
|
|
26180
|
-
}
|
|
26181
|
-
await beginInitTracking(db, projectId, "farm_provisioning");
|
|
26182
|
-
try {
|
|
26183
|
-
const result = await requestProvision(farmId, projectId, sealedBundle, 6e4);
|
|
26184
|
-
if (result.ok) {
|
|
26185
|
-
await db.run(`UPDATE projects SET farm_id = ? WHERE id = ?`, farmId, projectId);
|
|
26186
|
-
await advanceInitPhase(db, projectId, "daemon_starting");
|
|
26187
|
-
requestFarmPoolSync(db, farmId).catch(
|
|
26188
|
-
(err) => app.log.warn({ farmId, err }, "farm pool sync after provision failed")
|
|
26189
|
-
);
|
|
26190
|
-
} else {
|
|
26191
|
-
await recordInitError(db, projectId, `farm provisioning failed: ${result.error ?? "unknown error"}`);
|
|
26192
|
-
}
|
|
26193
|
-
return result;
|
|
26194
|
-
} catch (err) {
|
|
26195
|
-
const reason = err instanceof Error ? err.message : "provision_failed";
|
|
26196
|
-
await recordInitError(
|
|
26197
|
-
db,
|
|
26198
|
-
projectId,
|
|
26199
|
-
reason === "farm_offline" ? "the hosting farm is offline" : reason === "farm_timeout" ? "the hosting farm did not respond in time" : `farm provisioning failed: ${reason}`
|
|
26200
|
-
);
|
|
26201
|
-
const status = reason === "farm_offline" ? 409 : reason === "farm_timeout" ? 504 : 500;
|
|
26202
|
-
return reply.status(status).send({ error: reason });
|
|
26203
|
-
}
|
|
26204
|
-
}
|
|
26205
|
-
);
|
|
26206
26167
|
}
|
|
26207
26168
|
var init_farm2 = __esm({
|
|
26208
26169
|
"src/routes/farm.ts"() {
|
|
26209
26170
|
"use strict";
|
|
26210
26171
|
init_sealedbox();
|
|
26211
26172
|
init_abuse_throttle();
|
|
26212
|
-
init_cli_auth();
|
|
26213
|
-
init_auth();
|
|
26214
|
-
init_users();
|
|
26215
|
-
init_init_status();
|
|
26216
26173
|
init_ws();
|
|
26217
|
-
|
|
26174
|
+
init_route_registry();
|
|
26218
26175
|
}
|
|
26219
26176
|
});
|
|
26220
26177
|
|
|
26221
26178
|
// src/runner-reg-tokens.ts
|
|
26222
26179
|
import { createHash as createHash8 } from "node:crypto";
|
|
26223
|
-
import { nanoid as
|
|
26180
|
+
import { nanoid as nanoid26 } from "nanoid";
|
|
26224
26181
|
function hashRunnerRegToken(secret) {
|
|
26225
26182
|
return createHash8("sha256").update(secret, "utf8").digest("hex");
|
|
26226
26183
|
}
|
|
26227
26184
|
async function issueRunnerRegToken(db, email) {
|
|
26228
26185
|
const normalized = email.trim().toLowerCase();
|
|
26229
|
-
const secret = `rnrreg_${
|
|
26186
|
+
const secret = `rnrreg_${nanoid26(48)}`;
|
|
26230
26187
|
await db.tx(async (db2) => {
|
|
26231
26188
|
await db2.run(`UPDATE runner_reg_tokens SET revoked_at = ? WHERE email = ? AND revoked_at IS NULL`, Date.now(), normalized);
|
|
26232
26189
|
await db2.run(`INSERT INTO runner_reg_tokens (token_hash, email, user_id, created_at)
|
|
@@ -26262,130 +26219,8 @@ var init_runner_reg_tokens = __esm({
|
|
|
26262
26219
|
}
|
|
26263
26220
|
});
|
|
26264
26221
|
|
|
26265
|
-
// src/runner/provision.ts
|
|
26266
|
-
import { nanoid as nanoid29 } from "nanoid";
|
|
26267
|
-
function handleRunnerResult(msg) {
|
|
26268
|
-
const type = msg["type"];
|
|
26269
|
-
if (typeof type !== "string" || !RESULT_TYPES2.has(type)) return;
|
|
26270
|
-
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
26271
|
-
const p = pending3.get(requestId);
|
|
26272
|
-
if (!p) return;
|
|
26273
|
-
clearTimeout(p.timer);
|
|
26274
|
-
pending3.delete(requestId);
|
|
26275
|
-
p.resolve(msg);
|
|
26276
|
-
}
|
|
26277
|
-
function awaitResult2(requestId, send, timeoutMs) {
|
|
26278
|
-
if (!send()) return Promise.reject(new Error("runner_offline"));
|
|
26279
|
-
return new Promise((resolve4, reject) => {
|
|
26280
|
-
const timer = setTimeout(() => {
|
|
26281
|
-
pending3.delete(requestId);
|
|
26282
|
-
reject(new Error("runner_timeout"));
|
|
26283
|
-
}, timeoutMs);
|
|
26284
|
-
pending3.set(requestId, { resolve: resolve4, reject, timer });
|
|
26285
|
-
});
|
|
26286
|
-
}
|
|
26287
|
-
function requestGhRunnersProvision(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
26288
|
-
const requestId = nanoid29(16);
|
|
26289
|
-
return awaitResult2(
|
|
26290
|
-
requestId,
|
|
26291
|
-
() => sendToRunner(runnerId, { type: "provision_gh_runners", requestId, projectId, sealedBundle }),
|
|
26292
|
-
timeoutMs
|
|
26293
|
-
);
|
|
26294
|
-
}
|
|
26295
|
-
function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
26296
|
-
const requestId = nanoid29(16);
|
|
26297
|
-
return awaitResult2(
|
|
26298
|
-
requestId,
|
|
26299
|
-
() => sendToRunner(runnerId, {
|
|
26300
|
-
type: "delete_gh_runners",
|
|
26301
|
-
requestId,
|
|
26302
|
-
projectId,
|
|
26303
|
-
...sealedBundle ? { sealedBundle } : {}
|
|
26304
|
-
}),
|
|
26305
|
-
timeoutMs
|
|
26306
|
-
);
|
|
26307
|
-
}
|
|
26308
|
-
var pending3, RESULT_TYPES2;
|
|
26309
|
-
var init_provision2 = __esm({
|
|
26310
|
-
"src/runner/provision.ts"() {
|
|
26311
|
-
"use strict";
|
|
26312
|
-
init_ws();
|
|
26313
|
-
pending3 = /* @__PURE__ */ new Map();
|
|
26314
|
-
RESULT_TYPES2 = /* @__PURE__ */ new Set(["provision_gh_runners_result", "delete_gh_runners_result"]);
|
|
26315
|
-
}
|
|
26316
|
-
});
|
|
26317
|
-
|
|
26318
|
-
// src/runner/bindings.ts
|
|
26319
|
-
function parseGhRunnerBindings(json) {
|
|
26320
|
-
if (!json) return [];
|
|
26321
|
-
try {
|
|
26322
|
-
const parsed = JSON.parse(json);
|
|
26323
|
-
return Array.isArray(parsed.bindings) ? parsed.bindings : [];
|
|
26324
|
-
} catch {
|
|
26325
|
-
return [];
|
|
26326
|
-
}
|
|
26327
|
-
}
|
|
26328
|
-
function serialize(bindings) {
|
|
26329
|
-
return JSON.stringify({ bindings });
|
|
26330
|
-
}
|
|
26331
|
-
async function upsertGhRunnerBinding(db, projectId, binding) {
|
|
26332
|
-
await db.tx(async (tx) => {
|
|
26333
|
-
const row = await tx.get(
|
|
26334
|
-
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
26335
|
-
projectId
|
|
26336
|
-
);
|
|
26337
|
-
if (!row) return;
|
|
26338
|
-
const bindings = parseGhRunnerBindings(row.bindingsJson).filter(
|
|
26339
|
-
(b) => !(b.owner === binding.owner && b.repo === binding.repo)
|
|
26340
|
-
);
|
|
26341
|
-
bindings.push(binding);
|
|
26342
|
-
await tx.run(
|
|
26343
|
-
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
26344
|
-
serialize(bindings),
|
|
26345
|
-
projectId
|
|
26346
|
-
);
|
|
26347
|
-
});
|
|
26348
|
-
}
|
|
26349
|
-
async function removeGhRunnerBinding(db, projectId, owner, repo) {
|
|
26350
|
-
return db.tx(async (tx) => {
|
|
26351
|
-
const row = await tx.get(
|
|
26352
|
-
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
26353
|
-
projectId
|
|
26354
|
-
);
|
|
26355
|
-
if (!row) return false;
|
|
26356
|
-
const bindings = parseGhRunnerBindings(row.bindingsJson);
|
|
26357
|
-
const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
|
|
26358
|
-
if (kept.length === bindings.length) return false;
|
|
26359
|
-
await tx.run(
|
|
26360
|
-
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
26361
|
-
serialize(kept),
|
|
26362
|
-
projectId
|
|
26363
|
-
);
|
|
26364
|
-
return true;
|
|
26365
|
-
});
|
|
26366
|
-
}
|
|
26367
|
-
async function runnerBoundProjectCount(db, runnerId) {
|
|
26368
|
-
const rows = await db.all(
|
|
26369
|
-
`SELECT gh_runner_bindings_json AS json FROM projects WHERE gh_runner_bindings_json IS NOT NULL`
|
|
26370
|
-
);
|
|
26371
|
-
let n = 0;
|
|
26372
|
-
for (const r of rows) {
|
|
26373
|
-
try {
|
|
26374
|
-
const bindings = JSON.parse(r.json).bindings ?? [];
|
|
26375
|
-
if (bindings.some((b) => b.runnerId === runnerId)) n++;
|
|
26376
|
-
} catch {
|
|
26377
|
-
}
|
|
26378
|
-
}
|
|
26379
|
-
return n;
|
|
26380
|
-
}
|
|
26381
|
-
var init_bindings = __esm({
|
|
26382
|
-
"src/runner/bindings.ts"() {
|
|
26383
|
-
"use strict";
|
|
26384
|
-
}
|
|
26385
|
-
});
|
|
26386
|
-
|
|
26387
26222
|
// src/routes/runner.ts
|
|
26388
|
-
import { nanoid as
|
|
26223
|
+
import { nanoid as nanoid27 } from "nanoid";
|
|
26389
26224
|
async function listRunners(db, viewer) {
|
|
26390
26225
|
const online = new Set(listConnectedRunnerIds());
|
|
26391
26226
|
const rows = await db.all(`SELECT id, name, public_key as publicKey,
|
|
@@ -26438,8 +26273,15 @@ async function runnerRoutes(app, deps) {
|
|
|
26438
26273
|
const { db, config } = deps;
|
|
26439
26274
|
const registerByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 10 });
|
|
26440
26275
|
app.post(
|
|
26441
|
-
"/runner/register",
|
|
26276
|
+
"/node/v1/runner/register",
|
|
26442
26277
|
{
|
|
26278
|
+
...routeMeta({
|
|
26279
|
+
surface: "node",
|
|
26280
|
+
operationId: "node.runner.register",
|
|
26281
|
+
authPolicy: "public",
|
|
26282
|
+
stability: "preview",
|
|
26283
|
+
summary: "Register a RUNNER host with a personal registration token"
|
|
26284
|
+
}),
|
|
26443
26285
|
schema: {
|
|
26444
26286
|
body: {
|
|
26445
26287
|
type: "object",
|
|
@@ -26462,8 +26304,8 @@ async function runnerRoutes(app, deps) {
|
|
|
26462
26304
|
if (!owner) {
|
|
26463
26305
|
return reply.status(401).send({ error: "bad_registration_token" });
|
|
26464
26306
|
}
|
|
26465
|
-
const id = `rnr_${
|
|
26466
|
-
const token =
|
|
26307
|
+
const id = `rnr_${nanoid27(16)}`;
|
|
26308
|
+
const token = nanoid27(48);
|
|
26467
26309
|
const now = Date.now();
|
|
26468
26310
|
const publicKey = req.body.publicKey ?? null;
|
|
26469
26311
|
const fingerprint2 = publicKey ? publicKeyFingerprint(publicKey) : null;
|
|
@@ -26531,108 +26373,6 @@ async function runnerRoutes(app, deps) {
|
|
|
26531
26373
|
return { ok: true };
|
|
26532
26374
|
}
|
|
26533
26375
|
);
|
|
26534
|
-
const ghRunnerGuard = async (authorization, reply, runnerId, projectId) => {
|
|
26535
|
-
const auth = await resolveCliAuth(db, config, authorization);
|
|
26536
|
-
if (!auth) {
|
|
26537
|
-
reply.status(401).send({ error: "unauthorized" });
|
|
26538
|
-
return null;
|
|
26539
|
-
}
|
|
26540
|
-
if (!await canActOnProject(db, config, auth, projectId)) {
|
|
26541
|
-
reply.status(403).send({ error: "forbidden" });
|
|
26542
|
-
return null;
|
|
26543
|
-
}
|
|
26544
|
-
if (!await db.get(`SELECT id FROM runners WHERE id = ?`, runnerId)) {
|
|
26545
|
-
reply.status(404).send({ error: "unknown_runner" });
|
|
26546
|
-
return null;
|
|
26547
|
-
}
|
|
26548
|
-
if (!isSuperUser(config, auth.email) && !await viewerOwnsRunner(db, { email: auth.email, userId: auth.userId }, runnerId)) {
|
|
26549
|
-
reply.status(403).send({ error: "runner_not_owned" });
|
|
26550
|
-
return null;
|
|
26551
|
-
}
|
|
26552
|
-
if (!await db.get(`SELECT id FROM projects WHERE id = ?`, projectId)) {
|
|
26553
|
-
reply.status(404).send({ error: "unknown_project" });
|
|
26554
|
-
return null;
|
|
26555
|
-
}
|
|
26556
|
-
return auth;
|
|
26557
|
-
};
|
|
26558
|
-
const ghRelayFailure = (reply, err) => {
|
|
26559
|
-
const reason = err instanceof Error ? err.message : "gh_runner_relay_failed";
|
|
26560
|
-
const status = reason === "runner_offline" ? 409 : reason === "runner_timeout" ? 504 : 500;
|
|
26561
|
-
return reply.status(status).send({ error: reason });
|
|
26562
|
-
};
|
|
26563
|
-
app.post(
|
|
26564
|
-
"/runners/:runnerId/gh-runners",
|
|
26565
|
-
{
|
|
26566
|
-
schema: {
|
|
26567
|
-
body: {
|
|
26568
|
-
type: "object",
|
|
26569
|
-
required: ["projectId", "sealedBundle"],
|
|
26570
|
-
properties: {
|
|
26571
|
-
projectId: { type: "string" },
|
|
26572
|
-
sealedBundle: { type: "string", minLength: 1 }
|
|
26573
|
-
}
|
|
26574
|
-
}
|
|
26575
|
-
}
|
|
26576
|
-
},
|
|
26577
|
-
async (req, reply) => {
|
|
26578
|
-
const { runnerId } = req.params;
|
|
26579
|
-
const { projectId, sealedBundle } = req.body;
|
|
26580
|
-
if (!await ghRunnerGuard(req.headers.authorization, reply, runnerId, projectId)) return reply;
|
|
26581
|
-
try {
|
|
26582
|
-
const result = await requestGhRunnersProvision(runnerId, projectId, sealedBundle, 12e4);
|
|
26583
|
-
if (result.ok) {
|
|
26584
|
-
const connectedAt = Date.now();
|
|
26585
|
-
for (const r of result.runners ?? []) {
|
|
26586
|
-
await upsertGhRunnerBinding(db, projectId, {
|
|
26587
|
-
owner: r.owner,
|
|
26588
|
-
repo: r.repo,
|
|
26589
|
-
runnerId,
|
|
26590
|
-
connectedAt,
|
|
26591
|
-
status: r.status,
|
|
26592
|
-
...r.error ? { error: r.error } : {},
|
|
26593
|
-
...r.labels ? { labels: r.labels } : {}
|
|
26594
|
-
});
|
|
26595
|
-
}
|
|
26596
|
-
}
|
|
26597
|
-
return result;
|
|
26598
|
-
} catch (err) {
|
|
26599
|
-
return ghRelayFailure(reply, err);
|
|
26600
|
-
}
|
|
26601
|
-
}
|
|
26602
|
-
);
|
|
26603
|
-
app.post(
|
|
26604
|
-
"/runners/:runnerId/gh-runners/teardown",
|
|
26605
|
-
{
|
|
26606
|
-
schema: {
|
|
26607
|
-
body: {
|
|
26608
|
-
type: "object",
|
|
26609
|
-
required: ["projectId"],
|
|
26610
|
-
properties: {
|
|
26611
|
-
projectId: { type: "string" },
|
|
26612
|
-
sealedBundle: { type: "string", minLength: 1 },
|
|
26613
|
-
// DEV-530: the one repo being disconnected — plaintext public
|
|
26614
|
-
// identifiers (not secrets), so the HUB can drop its binding.
|
|
26615
|
-
owner: { type: "string", minLength: 1 },
|
|
26616
|
-
repo: { type: "string", minLength: 1 }
|
|
26617
|
-
}
|
|
26618
|
-
}
|
|
26619
|
-
}
|
|
26620
|
-
},
|
|
26621
|
-
async (req, reply) => {
|
|
26622
|
-
const { runnerId } = req.params;
|
|
26623
|
-
const { projectId, sealedBundle, owner, repo } = req.body;
|
|
26624
|
-
if (!await ghRunnerGuard(req.headers.authorization, reply, runnerId, projectId)) return reply;
|
|
26625
|
-
try {
|
|
26626
|
-
const result = await requestGhRunnersDelete(runnerId, projectId, sealedBundle, 12e4);
|
|
26627
|
-
if (result.ok && owner && repo) {
|
|
26628
|
-
await removeGhRunnerBinding(db, projectId, owner, repo);
|
|
26629
|
-
}
|
|
26630
|
-
return result;
|
|
26631
|
-
} catch (err) {
|
|
26632
|
-
return ghRelayFailure(reply, err);
|
|
26633
|
-
}
|
|
26634
|
-
}
|
|
26635
|
-
);
|
|
26636
26376
|
}
|
|
26637
26377
|
var init_runner2 = __esm({
|
|
26638
26378
|
"src/routes/runner.ts"() {
|
|
@@ -26644,8 +26384,93 @@ var init_runner2 = __esm({
|
|
|
26644
26384
|
init_users();
|
|
26645
26385
|
init_runner_reg_tokens();
|
|
26646
26386
|
init_ws();
|
|
26647
|
-
|
|
26648
|
-
|
|
26387
|
+
init_route_registry();
|
|
26388
|
+
}
|
|
26389
|
+
});
|
|
26390
|
+
|
|
26391
|
+
// src/farm/pool-spec.ts
|
|
26392
|
+
var pool_spec_exports = {};
|
|
26393
|
+
__export(pool_spec_exports, {
|
|
26394
|
+
buildSyncWorkerPoolMessage: () => buildSyncWorkerPoolMessage,
|
|
26395
|
+
computeFarmPoolSpec: () => computeFarmPoolSpec,
|
|
26396
|
+
reconcileFarmPools: () => reconcileFarmPools
|
|
26397
|
+
});
|
|
26398
|
+
import { nanoid as nanoid28 } from "nanoid";
|
|
26399
|
+
async function computeFarmPoolSpec(db, farmId) {
|
|
26400
|
+
const rows = await db.all(
|
|
26401
|
+
`SELECT fa.user_id as userId, fa.max_sandboxes as quota,
|
|
26402
|
+
p.id as projectId
|
|
26403
|
+
FROM farm_assignments fa
|
|
26404
|
+
JOIN projects p ON p.farm_id = fa.farm_id AND p.owner_user_id = fa.user_id
|
|
26405
|
+
WHERE fa.farm_id = ?
|
|
26406
|
+
ORDER BY fa.assigned_at ASC, p.id ASC`,
|
|
26407
|
+
farmId
|
|
26408
|
+
);
|
|
26409
|
+
const ceilingRow = await db.get(
|
|
26410
|
+
`SELECT max_sandboxes as ceiling FROM farms WHERE id = ?`,
|
|
26411
|
+
farmId
|
|
26412
|
+
);
|
|
26413
|
+
const ceiling = ceilingRow ? Number(ceilingRow.ceiling) : 16;
|
|
26414
|
+
const byAccount = /* @__PURE__ */ new Map();
|
|
26415
|
+
for (const r of rows) {
|
|
26416
|
+
const acc = byAccount.get(r.userId) ?? { quota: Number(r.quota), projectIds: [] };
|
|
26417
|
+
acc.projectIds.push(r.projectId);
|
|
26418
|
+
byAccount.set(r.userId, acc);
|
|
26419
|
+
}
|
|
26420
|
+
const spec = [];
|
|
26421
|
+
let remaining = ceiling;
|
|
26422
|
+
for (const [userId, acc] of byAccount) {
|
|
26423
|
+
const quota = Math.max(0, Math.min(acc.quota, remaining));
|
|
26424
|
+
remaining -= quota;
|
|
26425
|
+
spec.push({ userId, projectIds: acc.projectIds, quota });
|
|
26426
|
+
}
|
|
26427
|
+
return spec;
|
|
26428
|
+
}
|
|
26429
|
+
function buildSyncWorkerPoolMessage(accounts) {
|
|
26430
|
+
return { type: "sync_worker_pool", requestId: nanoid28(16), accounts };
|
|
26431
|
+
}
|
|
26432
|
+
async function reconcileFarmPools(db, send, log) {
|
|
26433
|
+
const farms = await db.all(`SELECT id FROM farms`);
|
|
26434
|
+
const live = new Set(farms.map((f) => f.id));
|
|
26435
|
+
for (const id of [...starvationReported]) {
|
|
26436
|
+
if (!live.has(id)) starvationReported.delete(id);
|
|
26437
|
+
}
|
|
26438
|
+
const starved = [];
|
|
26439
|
+
let pushed = 0;
|
|
26440
|
+
for (const farm of farms) {
|
|
26441
|
+
const accounts = await computeFarmPoolSpec(db, farm.id);
|
|
26442
|
+
if (accounts.length > 0) {
|
|
26443
|
+
const sent = send(farm.id, buildSyncWorkerPoolMessage(accounts));
|
|
26444
|
+
if (sent !== false) pushed++;
|
|
26445
|
+
starvationReported.delete(farm.id);
|
|
26446
|
+
continue;
|
|
26447
|
+
}
|
|
26448
|
+
const queued = await db.get(
|
|
26449
|
+
`SELECT COUNT(*) AS n FROM jobs j
|
|
26450
|
+
JOIN projects p ON p.id = j.project_id
|
|
26451
|
+
WHERE j.state = 'queued' AND p.farm_id = ?`,
|
|
26452
|
+
farm.id
|
|
26453
|
+
);
|
|
26454
|
+
if (Number(queued.n) === 0) {
|
|
26455
|
+
starvationReported.delete(farm.id);
|
|
26456
|
+
continue;
|
|
26457
|
+
}
|
|
26458
|
+
starved.push(farm.id);
|
|
26459
|
+
if (!starvationReported.has(farm.id)) {
|
|
26460
|
+
starvationReported.add(farm.id);
|
|
26461
|
+
log.warn(
|
|
26462
|
+
{ farmId: farm.id, queued: Number(queued.n) },
|
|
26463
|
+
"farm has NO worker-pool accounts while jobs are queued \u2014 assignments missing?"
|
|
26464
|
+
);
|
|
26465
|
+
}
|
|
26466
|
+
}
|
|
26467
|
+
return { farms: farms.length, pushed, starved };
|
|
26468
|
+
}
|
|
26469
|
+
var starvationReported;
|
|
26470
|
+
var init_pool_spec = __esm({
|
|
26471
|
+
"src/farm/pool-spec.ts"() {
|
|
26472
|
+
"use strict";
|
|
26473
|
+
starvationReported = /* @__PURE__ */ new Set();
|
|
26649
26474
|
}
|
|
26650
26475
|
});
|
|
26651
26476
|
|
|
@@ -26905,25 +26730,25 @@ async function requestGrantsFrom(spec, recipient, bindingVersion, log, timeoutMs
|
|
|
26905
26730
|
const requestId = msg.requestId;
|
|
26906
26731
|
return await new Promise((resolve4) => {
|
|
26907
26732
|
const timer = setTimeout(() => {
|
|
26908
|
-
|
|
26733
|
+
pending2.delete(requestId);
|
|
26909
26734
|
log.warn({ projectId: spec.projectId, jobId: spec.jobId, sandboxId: recipient.sandboxId }, "credential grants: CLI did not answer in time");
|
|
26910
26735
|
resolve4(null);
|
|
26911
26736
|
}, timeoutMs);
|
|
26912
|
-
|
|
26737
|
+
pending2.set(requestId, {
|
|
26913
26738
|
projectId: spec.projectId,
|
|
26914
26739
|
jobId: spec.jobId,
|
|
26915
26740
|
sandboxId: recipient.sandboxId,
|
|
26916
26741
|
holder,
|
|
26917
26742
|
resolve: (answer) => {
|
|
26918
26743
|
clearTimeout(timer);
|
|
26919
|
-
|
|
26744
|
+
pending2.delete(requestId);
|
|
26920
26745
|
resolve4(answer);
|
|
26921
26746
|
}
|
|
26922
26747
|
});
|
|
26923
26748
|
const sent = holder.kind === "provider" ? sendToAgentLoginProvider(holder.ownerKey, holder.providerId, msg) : sendToProjectCli(spec.projectId, msg);
|
|
26924
26749
|
if (!sent) {
|
|
26925
26750
|
clearTimeout(timer);
|
|
26926
|
-
|
|
26751
|
+
pending2.delete(requestId);
|
|
26927
26752
|
resolve4(null);
|
|
26928
26753
|
}
|
|
26929
26754
|
});
|
|
@@ -26934,7 +26759,7 @@ function acceptCredentialGrants(msg, from) {
|
|
|
26934
26759
|
return { ok: false, reason: "malformed" };
|
|
26935
26760
|
}
|
|
26936
26761
|
if (typeof from === "string" && m.projectId !== from) return { ok: false, reason: "cross-project" };
|
|
26937
|
-
const req =
|
|
26762
|
+
const req = pending2.get(m.requestId);
|
|
26938
26763
|
if (!req) return { ok: false, reason: NO_PENDING_REQUEST };
|
|
26939
26764
|
if (typeof from === "string") {
|
|
26940
26765
|
if (req.holder.kind !== "client") return { ok: false, reason: "answer from a holder that was not asked" };
|
|
@@ -27140,15 +26965,15 @@ async function sweepExpiredGrants(db, now = Date.now()) {
|
|
|
27140
26965
|
return changes;
|
|
27141
26966
|
}
|
|
27142
26967
|
function resolvePendingGrantRequest(requestId, answer) {
|
|
27143
|
-
const req =
|
|
26968
|
+
const req = pending2.get(requestId);
|
|
27144
26969
|
if (!req) return false;
|
|
27145
26970
|
req.resolve(answer);
|
|
27146
26971
|
return true;
|
|
27147
26972
|
}
|
|
27148
26973
|
function pendingGrantRequests() {
|
|
27149
|
-
return
|
|
26974
|
+
return pending2.size;
|
|
27150
26975
|
}
|
|
27151
|
-
var GRANT_REQUEST_TIMEOUT_MS,
|
|
26976
|
+
var GRANT_REQUEST_TIMEOUT_MS, pending2, NO_PENDING_REQUEST, LATE_ANSWER_TTL_MS, lateAnswers;
|
|
27152
26977
|
var init_credential_grants = __esm({
|
|
27153
26978
|
"src/tracker/credential-grants.ts"() {
|
|
27154
26979
|
"use strict";
|
|
@@ -27159,7 +26984,7 @@ var init_credential_grants = __esm({
|
|
|
27159
26984
|
init_ws();
|
|
27160
26985
|
init_sandbox_keys();
|
|
27161
26986
|
GRANT_REQUEST_TIMEOUT_MS = 6e3;
|
|
27162
|
-
|
|
26987
|
+
pending2 = /* @__PURE__ */ new Map();
|
|
27163
26988
|
NO_PENDING_REQUEST = "no pending request";
|
|
27164
26989
|
LATE_ANSWER_TTL_MS = 6e4;
|
|
27165
26990
|
lateAnswers = /* @__PURE__ */ new Map();
|
|
@@ -27739,23 +27564,11 @@ function installSocketKeepalive(socket, periodMs) {
|
|
|
27739
27564
|
}
|
|
27740
27565
|
async function wsRoutes(app, deps) {
|
|
27741
27566
|
const { db, config } = deps;
|
|
27742
|
-
async function authenticateSocket(req,
|
|
27743
|
-
|
|
27744
|
-
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
return resolved;
|
|
27748
|
-
}
|
|
27749
|
-
return null;
|
|
27750
|
-
}
|
|
27751
|
-
const warnedComponents = /* @__PURE__ */ new Set();
|
|
27752
|
-
function warnAboutQueryCredential(component, remote) {
|
|
27753
|
-
if (warnedComponents.has(component)) return;
|
|
27754
|
-
warnedComponents.add(component);
|
|
27755
|
-
app.log.warn(
|
|
27756
|
-
{ component, remote },
|
|
27757
|
-
"node socket authenticated from the query string \u2014 the credential lands in proxy logs; update the component to send it in the Authorization header (this is logged once)"
|
|
27758
|
-
);
|
|
27567
|
+
async function authenticateSocket(req, resolve4) {
|
|
27568
|
+
const token = readNodeSocketSecret(req.headers);
|
|
27569
|
+
if (token === null) return null;
|
|
27570
|
+
const resolved = await resolve4(token);
|
|
27571
|
+
return resolved === null || resolved === void 0 ? null : resolved;
|
|
27759
27572
|
}
|
|
27760
27573
|
const cliSocket = async (socket, req) => {
|
|
27761
27574
|
let boundProjectId = null;
|
|
@@ -28092,7 +27905,7 @@ async function wsRoutes(app, deps) {
|
|
|
28092
27905
|
app.log.info({ remote: req.ip }, "CLI socket closed (never registered)");
|
|
28093
27906
|
}
|
|
28094
27907
|
});
|
|
28095
|
-
auth = await authenticateSocket(req,
|
|
27908
|
+
auth = await authenticateSocket(req, (token) => authFromSecret(db, config, token));
|
|
28096
27909
|
if (!auth) {
|
|
28097
27910
|
socket.close(1008, "unauthorized");
|
|
28098
27911
|
return;
|
|
@@ -28110,7 +27923,7 @@ async function wsRoutes(app, deps) {
|
|
|
28110
27923
|
void pump();
|
|
28111
27924
|
};
|
|
28112
27925
|
const sandboxSocket = async (socket, req) => {
|
|
28113
|
-
const sandbox = await authenticateSocket(req,
|
|
27926
|
+
const sandbox = await authenticateSocket(req, (token) => findSandboxByToken(db, token));
|
|
28114
27927
|
if (!sandbox) {
|
|
28115
27928
|
socket.close(1008, "unauthorized");
|
|
28116
27929
|
return;
|
|
@@ -28228,7 +28041,7 @@ async function wsRoutes(app, deps) {
|
|
|
28228
28041
|
});
|
|
28229
28042
|
};
|
|
28230
28043
|
const farmSocket = async (socket, req) => {
|
|
28231
|
-
const farm = await authenticateSocket(req,
|
|
28044
|
+
const farm = await authenticateSocket(req, (token) => findFarmByToken(db, token));
|
|
28232
28045
|
if (!farm) {
|
|
28233
28046
|
socket.close(1008, "unauthorized");
|
|
28234
28047
|
return;
|
|
@@ -28320,7 +28133,7 @@ async function wsRoutes(app, deps) {
|
|
|
28320
28133
|
});
|
|
28321
28134
|
};
|
|
28322
28135
|
const runnerSocket = async (socket, req) => {
|
|
28323
|
-
const runner = await authenticateSocket(req,
|
|
28136
|
+
const runner = await authenticateSocket(req, (token) => findRunnerByToken(db, token));
|
|
28324
28137
|
if (!runner) {
|
|
28325
28138
|
socket.close(1008, "unauthorized");
|
|
28326
28139
|
return;
|
|
@@ -28452,10 +28265,12 @@ async function wsRoutes(app, deps) {
|
|
|
28452
28265
|
}
|
|
28453
28266
|
const auth = parseNodeAuth(parsed);
|
|
28454
28267
|
if (auth !== null) {
|
|
28455
|
-
|
|
28268
|
+
const withFrameCredential = { ...req, headers: { ...req.headers, authorization: `Bearer ${auth.token}` } };
|
|
28269
|
+
if (auth.component !== component || !await authenticate(withFrameCredential)) {
|
|
28456
28270
|
refuse("unauthenticated");
|
|
28457
28271
|
return;
|
|
28458
28272
|
}
|
|
28273
|
+
effectiveReq = withFrameCredential;
|
|
28459
28274
|
authenticated = true;
|
|
28460
28275
|
return;
|
|
28461
28276
|
}
|
|
@@ -28492,7 +28307,7 @@ async function wsRoutes(app, deps) {
|
|
|
28492
28307
|
socket.off("message", listener);
|
|
28493
28308
|
socket.pause();
|
|
28494
28309
|
try {
|
|
28495
|
-
await inner(socket,
|
|
28310
|
+
await inner(socket, effectiveReq);
|
|
28496
28311
|
} catch (err) {
|
|
28497
28312
|
app.log.error({ err, component }, "node socket handler failed after handshake");
|
|
28498
28313
|
socket.close(1011, "handler_failed");
|
|
@@ -28503,6 +28318,7 @@ async function wsRoutes(app, deps) {
|
|
|
28503
28318
|
socket.resume();
|
|
28504
28319
|
};
|
|
28505
28320
|
let authenticated = false;
|
|
28321
|
+
let effectiveReq = req;
|
|
28506
28322
|
const ready = authenticate(req).then((ok2) => {
|
|
28507
28323
|
authenticated = ok2;
|
|
28508
28324
|
});
|
|
@@ -28536,7 +28352,6 @@ async function wsRoutes(app, deps) {
|
|
|
28536
28352
|
});
|
|
28537
28353
|
}
|
|
28538
28354
|
}
|
|
28539
|
-
app.get("/cli/connect", { websocket: true }, cliSocket);
|
|
28540
28355
|
app.get(
|
|
28541
28356
|
"/node/v1/cli/connect",
|
|
28542
28357
|
{
|
|
@@ -28550,9 +28365,8 @@ async function wsRoutes(app, deps) {
|
|
|
28550
28365
|
summary: "Control socket of a CLI instance"
|
|
28551
28366
|
})
|
|
28552
28367
|
},
|
|
28553
|
-
withNodeHandshake("cli", async (r) => await authenticateSocket(r,
|
|
28368
|
+
withNodeHandshake("cli", async (r) => await authenticateSocket(r, (token) => authFromSecret(db, config, token)) !== null, cliSocket)
|
|
28554
28369
|
);
|
|
28555
|
-
app.get("/sandbox/connect", { websocket: true }, sandboxSocket);
|
|
28556
28370
|
app.get(
|
|
28557
28371
|
"/node/v1/sandbox/connect",
|
|
28558
28372
|
{
|
|
@@ -28566,9 +28380,8 @@ async function wsRoutes(app, deps) {
|
|
|
28566
28380
|
summary: "Control socket of a SANDBOX instance"
|
|
28567
28381
|
})
|
|
28568
28382
|
},
|
|
28569
|
-
withNodeHandshake("sandbox", async (r) => await authenticateSocket(r,
|
|
28383
|
+
withNodeHandshake("sandbox", async (r) => await authenticateSocket(r, (token) => findSandboxByToken(db, token)) !== null, sandboxSocket)
|
|
28570
28384
|
);
|
|
28571
|
-
app.get("/farm/connect", { websocket: true }, farmSocket);
|
|
28572
28385
|
app.get(
|
|
28573
28386
|
"/node/v1/farm/connect",
|
|
28574
28387
|
{
|
|
@@ -28582,9 +28395,8 @@ async function wsRoutes(app, deps) {
|
|
|
28582
28395
|
summary: "Control socket of a FARM instance"
|
|
28583
28396
|
})
|
|
28584
28397
|
},
|
|
28585
|
-
withNodeHandshake("farm", async (r) => await authenticateSocket(r,
|
|
28398
|
+
withNodeHandshake("farm", async (r) => await authenticateSocket(r, (token) => findFarmByToken(db, token)) !== null, farmSocket)
|
|
28586
28399
|
);
|
|
28587
|
-
app.get("/runner/connect", { websocket: true }, runnerSocket);
|
|
28588
28400
|
app.get(
|
|
28589
28401
|
"/node/v1/runner/connect",
|
|
28590
28402
|
{
|
|
@@ -28598,7 +28410,7 @@ async function wsRoutes(app, deps) {
|
|
|
28598
28410
|
summary: "Control socket of a RUNNER instance"
|
|
28599
28411
|
})
|
|
28600
28412
|
},
|
|
28601
|
-
withNodeHandshake("runner", async (r) => await authenticateSocket(r,
|
|
28413
|
+
withNodeHandshake("runner", async (r) => await authenticateSocket(r, (token) => findRunnerByToken(db, token)) !== null, runnerSocket)
|
|
28602
28414
|
);
|
|
28603
28415
|
}
|
|
28604
28416
|
var agentLoginProviders, deliveryFollowers, projectCliConnections, projectLastSeenAt, projectLastDuplicateRegisterAt, sandboxConnections, farmConnections, runnerConnections, cliMessageHandlers, cliRegisterHandlers, cliDisconnectHandlers, farmMessageHandlers, runnerMessageHandlers, providerMessageHandlers, RECOGNIZED_NODE_CAPABILITIES, NODE_HANDSHAKE_BACKLOG, NODE_HANDSHAKE_TIMEOUT_MS;
|
|
@@ -29078,7 +28890,7 @@ function installOfflineSweeper(deps) {
|
|
|
29078
28890
|
init_dist5();
|
|
29079
28891
|
import { existsSync as existsSync4 } from "node:fs";
|
|
29080
28892
|
import staticPlugin from "@fastify/static";
|
|
29081
|
-
import { nanoid as
|
|
28893
|
+
import { nanoid as nanoid29 } from "nanoid";
|
|
29082
28894
|
|
|
29083
28895
|
// src/http/browser-routes.ts
|
|
29084
28896
|
init_route_definition();
|
|
@@ -29193,9 +29005,22 @@ var RETIRED_SOCKET_PATHS = [
|
|
|
29193
29005
|
"/farm/connect",
|
|
29194
29006
|
"/runner/connect"
|
|
29195
29007
|
];
|
|
29008
|
+
var RETIRED_NODE_PATHS = [
|
|
29009
|
+
"/jobs/claim",
|
|
29010
|
+
"/jobs/heartbeat",
|
|
29011
|
+
"/jobs/release",
|
|
29012
|
+
"/jobs/result",
|
|
29013
|
+
"/sandbox/register",
|
|
29014
|
+
"/sandbox/me",
|
|
29015
|
+
"/sandbox/agent-auth",
|
|
29016
|
+
"/sandbox/agent-auth/result",
|
|
29017
|
+
"/sandbox/reg-token/cli",
|
|
29018
|
+
"/farm/register",
|
|
29019
|
+
"/runner/register"
|
|
29020
|
+
];
|
|
29196
29021
|
function isRetiredClientPath(url, registeredPaths = []) {
|
|
29197
29022
|
const path = url.split("?")[0].split("#")[0];
|
|
29198
|
-
if (RETIRED_SOCKET_PATHS.includes(path)) return !registeredPaths.includes(path);
|
|
29023
|
+
if (RETIRED_SOCKET_PATHS.includes(path) || RETIRED_NODE_PATHS.includes(path)) return !registeredPaths.includes(path);
|
|
29199
29024
|
const retired = RETIRED_ROOTS.find((root) => path === root || path.startsWith(`${root}/`));
|
|
29200
29025
|
if (retired === void 0) return false;
|
|
29201
29026
|
return !registeredPaths.some((p) => p === retired || p.startsWith(`${retired}/`));
|
|
@@ -29239,7 +29064,7 @@ async function registerSpaHosts(app, opts) {
|
|
|
29239
29064
|
return onConsole ? reply.sendFile("index.html", adminDist) : reply.sendFile("index.html");
|
|
29240
29065
|
}
|
|
29241
29066
|
const failure = isRetiredClientPath(req.url, opts.registeredPaths ?? []) ? clientTooOldError() : errors.notFound();
|
|
29242
|
-
const rendered = renderError(failure, `req_${
|
|
29067
|
+
const rendered = renderError(failure, `req_${nanoid29(16)}`);
|
|
29243
29068
|
return reply.status(rendered.status).send(rendered.body);
|
|
29244
29069
|
});
|
|
29245
29070
|
}
|
|
@@ -29253,7 +29078,7 @@ import { createHash as createHash9, randomBytes as randomBytes3 } from "node:cry
|
|
|
29253
29078
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
29254
29079
|
import { join as join4 } from "node:path";
|
|
29255
29080
|
init_dist5();
|
|
29256
|
-
import { nanoid as
|
|
29081
|
+
import { nanoid as nanoid30 } from "nanoid";
|
|
29257
29082
|
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
29258
29083
|
var nonces = /* @__PURE__ */ new WeakMap();
|
|
29259
29084
|
function cspNonce(req) {
|
|
@@ -29342,7 +29167,7 @@ function installBrowserSecurity(app, config, options = {}) {
|
|
|
29342
29167
|
message: "This request did not come from an allowed origin.",
|
|
29343
29168
|
details: { reason: verdict.reason }
|
|
29344
29169
|
}),
|
|
29345
|
-
`req_${
|
|
29170
|
+
`req_${nanoid30(16)}`
|
|
29346
29171
|
);
|
|
29347
29172
|
await reply.status(rendered.status).send(rendered.body);
|
|
29348
29173
|
});
|
|
@@ -29470,7 +29295,7 @@ init_route_registry();
|
|
|
29470
29295
|
|
|
29471
29296
|
// src/routes/api-v1/context.ts
|
|
29472
29297
|
init_dist5();
|
|
29473
|
-
import { nanoid as
|
|
29298
|
+
import { nanoid as nanoid31 } from "nanoid";
|
|
29474
29299
|
|
|
29475
29300
|
// src/auth/resolve-principal.ts
|
|
29476
29301
|
init_auth();
|
|
@@ -29596,7 +29421,7 @@ async function resolveSession(db, req, surface, base) {
|
|
|
29596
29421
|
|
|
29597
29422
|
// src/routes/api-v1/context.ts
|
|
29598
29423
|
function requestIdOf(_req) {
|
|
29599
|
-
return `req_${
|
|
29424
|
+
return `req_${nanoid31(16)}`;
|
|
29600
29425
|
}
|
|
29601
29426
|
async function actorOr401(deps, req, reply, requestId) {
|
|
29602
29427
|
const resolution = await resolvePrincipal({
|
|
@@ -30009,6 +29834,49 @@ async function apiV1ReadRoutes(app, deps) {
|
|
|
30009
29834
|
}
|
|
30010
29835
|
}
|
|
30011
29836
|
);
|
|
29837
|
+
app.get(
|
|
29838
|
+
"/projects/:projectId/deliverables",
|
|
29839
|
+
routeMeta({
|
|
29840
|
+
surface: "application",
|
|
29841
|
+
operationId: "project.deliverables",
|
|
29842
|
+
authPolicy: "application_actor",
|
|
29843
|
+
resourcePolicy: "delivery.read",
|
|
29844
|
+
stability: "preview",
|
|
29845
|
+
summary: "Tasks whose branches a developer's clone can pull"
|
|
29846
|
+
}),
|
|
29847
|
+
async (req, reply) => {
|
|
29848
|
+
const requestId = requestIdOf(req);
|
|
29849
|
+
try {
|
|
29850
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
29851
|
+
if (!actor) return reply;
|
|
29852
|
+
const { projectId } = req.params;
|
|
29853
|
+
return await read.listProjectDeliverables(actor, { projectId });
|
|
29854
|
+
} catch (err) {
|
|
29855
|
+
return sendError(reply, err, requestId);
|
|
29856
|
+
}
|
|
29857
|
+
}
|
|
29858
|
+
);
|
|
29859
|
+
app.get(
|
|
29860
|
+
"/farms",
|
|
29861
|
+
routeMeta({
|
|
29862
|
+
surface: "application",
|
|
29863
|
+
operationId: "farm.list",
|
|
29864
|
+
authPolicy: "application_actor",
|
|
29865
|
+
resourcePolicy: "account.read",
|
|
29866
|
+
stability: "preview",
|
|
29867
|
+
summary: "Farms assigned to the caller's account"
|
|
29868
|
+
}),
|
|
29869
|
+
async (req, reply) => {
|
|
29870
|
+
const requestId = requestIdOf(req);
|
|
29871
|
+
try {
|
|
29872
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
29873
|
+
if (!actor) return reply;
|
|
29874
|
+
return await read.listFarms(actor);
|
|
29875
|
+
} catch (err) {
|
|
29876
|
+
return sendError(reply, err, requestId);
|
|
29877
|
+
}
|
|
29878
|
+
}
|
|
29879
|
+
);
|
|
30012
29880
|
app.get(
|
|
30013
29881
|
"/projects/:projectId/config-proposals",
|
|
30014
29882
|
routeMeta({
|
|
@@ -30558,6 +30426,101 @@ var OBSERVER_PASS_BODY = {
|
|
|
30558
30426
|
additionalProperties: false,
|
|
30559
30427
|
properties: { note: { type: "string", maxLength: 4e3 } }
|
|
30560
30428
|
};
|
|
30429
|
+
var CREATE_PROJECT_BODY = {
|
|
30430
|
+
type: "object",
|
|
30431
|
+
additionalProperties: false,
|
|
30432
|
+
required: ["projectId", "name", "automationLevel", "repos", "contextRepo"],
|
|
30433
|
+
properties: {
|
|
30434
|
+
projectId: { type: "string", minLength: 2, maxLength: 31 },
|
|
30435
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
30436
|
+
automationLevel: { type: "string", enum: ["assisted", "full"] },
|
|
30437
|
+
trackerProvider: { type: "string", enum: ["internal", "youtrack"] },
|
|
30438
|
+
trackerProjectKey: { type: "string", minLength: 1, maxLength: 64 },
|
|
30439
|
+
repos: {
|
|
30440
|
+
type: "array",
|
|
30441
|
+
maxItems: 100,
|
|
30442
|
+
items: {
|
|
30443
|
+
...REPO_BODY,
|
|
30444
|
+
properties: {
|
|
30445
|
+
...REPO_BODY.properties,
|
|
30446
|
+
mirror: {
|
|
30447
|
+
type: "object",
|
|
30448
|
+
additionalProperties: false,
|
|
30449
|
+
required: ["host", "owner", "repoName"],
|
|
30450
|
+
properties: {
|
|
30451
|
+
host: { type: "string", enum: ["github", "bitbucket", "gitlab"] },
|
|
30452
|
+
owner: { type: "string", minLength: 1, maxLength: 200 },
|
|
30453
|
+
repoName: { type: "string", minLength: 1, maxLength: 200 }
|
|
30454
|
+
},
|
|
30455
|
+
description: "ADR-0041: the mirror the pipeline pushes this repository's branches to."
|
|
30456
|
+
}
|
|
30457
|
+
}
|
|
30458
|
+
}
|
|
30459
|
+
},
|
|
30460
|
+
contextRepo: {
|
|
30461
|
+
type: "object",
|
|
30462
|
+
additionalProperties: false,
|
|
30463
|
+
required: ["host", "owner", "name", "cloneUrl", "branch"],
|
|
30464
|
+
properties: {
|
|
30465
|
+
host: { type: "string", enum: ["github", "bitbucket", "gitlab", "local"] },
|
|
30466
|
+
owner: { type: "string", minLength: 1, maxLength: 200 },
|
|
30467
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
30468
|
+
cloneUrl: { type: "string", minLength: 1, maxLength: 2e3 },
|
|
30469
|
+
branch: { type: "string", minLength: 1, maxLength: 250 }
|
|
30470
|
+
}
|
|
30471
|
+
},
|
|
30472
|
+
language: {
|
|
30473
|
+
type: "object",
|
|
30474
|
+
maxProperties: 20,
|
|
30475
|
+
additionalProperties: { type: "string", maxLength: 64 },
|
|
30476
|
+
description: "DEV-551: per-section language chosen at init."
|
|
30477
|
+
}
|
|
30478
|
+
}
|
|
30479
|
+
};
|
|
30480
|
+
var SEALED_HANDOFF_BODY = {
|
|
30481
|
+
type: "object",
|
|
30482
|
+
additionalProperties: false,
|
|
30483
|
+
required: ["projectId", "sealedBundle"],
|
|
30484
|
+
properties: {
|
|
30485
|
+
projectId: { type: "string", minLength: 1, maxLength: 64 },
|
|
30486
|
+
sealedBundle: { type: "string", minLength: 1 }
|
|
30487
|
+
}
|
|
30488
|
+
};
|
|
30489
|
+
var GH_RUNNERS_TEARDOWN_BODY = {
|
|
30490
|
+
type: "object",
|
|
30491
|
+
additionalProperties: false,
|
|
30492
|
+
required: ["projectId"],
|
|
30493
|
+
properties: {
|
|
30494
|
+
projectId: { type: "string", minLength: 1, maxLength: 64 },
|
|
30495
|
+
sealedBundle: { type: "string", minLength: 1 },
|
|
30496
|
+
repo: {
|
|
30497
|
+
type: "object",
|
|
30498
|
+
additionalProperties: false,
|
|
30499
|
+
required: ["owner", "repo"],
|
|
30500
|
+
properties: {
|
|
30501
|
+
owner: { type: "string", minLength: 1, maxLength: 200 },
|
|
30502
|
+
repo: { type: "string", minLength: 1, maxLength: 200 }
|
|
30503
|
+
},
|
|
30504
|
+
description: "DEV-530: the one repository being disconnected, so its binding is dropped."
|
|
30505
|
+
}
|
|
30506
|
+
}
|
|
30507
|
+
};
|
|
30508
|
+
var PROJECT_INDEX_BODY = {
|
|
30509
|
+
type: "object",
|
|
30510
|
+
additionalProperties: false,
|
|
30511
|
+
properties: {
|
|
30512
|
+
kind: { type: "string", enum: ["index", "backfill"], description: "Defaults to the ordinary index." }
|
|
30513
|
+
}
|
|
30514
|
+
};
|
|
30515
|
+
var INIT_STATUS_BODY = {
|
|
30516
|
+
type: "object",
|
|
30517
|
+
additionalProperties: false,
|
|
30518
|
+
required: ["phase"],
|
|
30519
|
+
properties: {
|
|
30520
|
+
phase: { type: "string", enum: ["context_cloning"] },
|
|
30521
|
+
error: { type: "string", minLength: 1, maxLength: 500 }
|
|
30522
|
+
}
|
|
30523
|
+
};
|
|
30561
30524
|
function responseOf(body) {
|
|
30562
30525
|
switch (body.decision) {
|
|
30563
30526
|
case "answer":
|
|
@@ -30962,6 +30925,191 @@ async function apiV1MutationRoutes(app, deps) {
|
|
|
30962
30925
|
}
|
|
30963
30926
|
}
|
|
30964
30927
|
);
|
|
30928
|
+
app.post(
|
|
30929
|
+
"/projects",
|
|
30930
|
+
routeMeta({
|
|
30931
|
+
surface: "application",
|
|
30932
|
+
operationId: "project.create",
|
|
30933
|
+
authPolicy: "application_actor",
|
|
30934
|
+
resourcePolicy: "account.manage",
|
|
30935
|
+
stability: "preview",
|
|
30936
|
+
summary: "Register a project under the caller's account",
|
|
30937
|
+
requestSchema: CREATE_PROJECT_BODY
|
|
30938
|
+
}),
|
|
30939
|
+
async (req, reply) => {
|
|
30940
|
+
const requestId = requestIdOf(req);
|
|
30941
|
+
try {
|
|
30942
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
30943
|
+
if (!actor) return reply;
|
|
30944
|
+
const body = req.body;
|
|
30945
|
+
const created = await commands.createProject(actor, {
|
|
30946
|
+
project: {
|
|
30947
|
+
projectId: body.projectId,
|
|
30948
|
+
name: body.name,
|
|
30949
|
+
automationLevel: body.automationLevel,
|
|
30950
|
+
trackerProvider: body.trackerProvider ?? "youtrack",
|
|
30951
|
+
trackerProjectKey: body.trackerProjectKey ?? null,
|
|
30952
|
+
repos: body.repos,
|
|
30953
|
+
contextRepo: body.contextRepo,
|
|
30954
|
+
language: body.language ?? null
|
|
30955
|
+
},
|
|
30956
|
+
idempotencyKey: requiredIdempotencyKey(req)
|
|
30957
|
+
});
|
|
30958
|
+
return reply.status(201).header("location", `/api/v1/projects/${encodeURIComponent(created.projectId)}`).send(created);
|
|
30959
|
+
} catch (err) {
|
|
30960
|
+
return sendError(reply, err, requestId);
|
|
30961
|
+
}
|
|
30962
|
+
}
|
|
30963
|
+
);
|
|
30964
|
+
app.post(
|
|
30965
|
+
"/farms/:farmId/provision",
|
|
30966
|
+
routeMeta({
|
|
30967
|
+
surface: "application",
|
|
30968
|
+
operationId: "farm.provision",
|
|
30969
|
+
authPolicy: "application_actor",
|
|
30970
|
+
resourcePolicy: "project.manage",
|
|
30971
|
+
stability: "preview",
|
|
30972
|
+
summary: "Hand a sealed project bundle to a farm and wait for its verdict",
|
|
30973
|
+
requestSchema: SEALED_HANDOFF_BODY
|
|
30974
|
+
}),
|
|
30975
|
+
async (req, reply) => {
|
|
30976
|
+
const requestId = requestIdOf(req);
|
|
30977
|
+
try {
|
|
30978
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
30979
|
+
if (!actor) return reply;
|
|
30980
|
+
const { farmId } = req.params;
|
|
30981
|
+
const { projectId, sealedBundle } = req.body;
|
|
30982
|
+
return await commands.provisionOnFarm(actor, { farmId, projectId, sealedBundle, idempotencyKey: requiredIdempotencyKey(req) });
|
|
30983
|
+
} catch (err) {
|
|
30984
|
+
return sendError(reply, err, requestId);
|
|
30985
|
+
}
|
|
30986
|
+
}
|
|
30987
|
+
);
|
|
30988
|
+
app.post(
|
|
30989
|
+
"/runners/:runnerId/gh-runners",
|
|
30990
|
+
routeMeta({
|
|
30991
|
+
surface: "application",
|
|
30992
|
+
operationId: "runner.provisionGhRunners",
|
|
30993
|
+
authPolicy: "application_actor",
|
|
30994
|
+
resourcePolicy: "project.manage",
|
|
30995
|
+
stability: "preview",
|
|
30996
|
+
summary: "Relay a sealed GitHub-runner bundle to a runner host of the caller's",
|
|
30997
|
+
requestSchema: SEALED_HANDOFF_BODY
|
|
30998
|
+
}),
|
|
30999
|
+
async (req, reply) => {
|
|
31000
|
+
const requestId = requestIdOf(req);
|
|
31001
|
+
try {
|
|
31002
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31003
|
+
if (!actor) return reply;
|
|
31004
|
+
const { runnerId } = req.params;
|
|
31005
|
+
const { projectId, sealedBundle } = req.body;
|
|
31006
|
+
return await commands.provisionGhRunners(actor, { runnerId, projectId, sealedBundle, idempotencyKey: requiredIdempotencyKey(req) });
|
|
31007
|
+
} catch (err) {
|
|
31008
|
+
return sendError(reply, err, requestId);
|
|
31009
|
+
}
|
|
31010
|
+
}
|
|
31011
|
+
);
|
|
31012
|
+
app.post(
|
|
31013
|
+
"/runners/:runnerId/gh-runners/teardown",
|
|
31014
|
+
routeMeta({
|
|
31015
|
+
surface: "application",
|
|
31016
|
+
operationId: "runner.teardownGhRunners",
|
|
31017
|
+
authPolicy: "application_actor",
|
|
31018
|
+
resourcePolicy: "project.manage",
|
|
31019
|
+
stability: "preview",
|
|
31020
|
+
summary: "Tear the project's GitHub runners down on that host",
|
|
31021
|
+
requestSchema: GH_RUNNERS_TEARDOWN_BODY
|
|
31022
|
+
}),
|
|
31023
|
+
async (req, reply) => {
|
|
31024
|
+
const requestId = requestIdOf(req);
|
|
31025
|
+
try {
|
|
31026
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31027
|
+
if (!actor) return reply;
|
|
31028
|
+
const { runnerId } = req.params;
|
|
31029
|
+
const body = req.body;
|
|
31030
|
+
return await commands.teardownGhRunners(actor, {
|
|
31031
|
+
runnerId,
|
|
31032
|
+
projectId: body.projectId,
|
|
31033
|
+
sealedBundle: body.sealedBundle,
|
|
31034
|
+
repo: body.repo,
|
|
31035
|
+
idempotencyKey: requiredIdempotencyKey(req)
|
|
31036
|
+
});
|
|
31037
|
+
} catch (err) {
|
|
31038
|
+
return sendError(reply, err, requestId);
|
|
31039
|
+
}
|
|
31040
|
+
}
|
|
31041
|
+
);
|
|
31042
|
+
app.post(
|
|
31043
|
+
"/projects/:projectId/index",
|
|
31044
|
+
routeMeta({
|
|
31045
|
+
surface: "application",
|
|
31046
|
+
operationId: "project.index",
|
|
31047
|
+
authPolicy: "application_actor",
|
|
31048
|
+
resourcePolicy: "project.manage",
|
|
31049
|
+
stability: "preview",
|
|
31050
|
+
summary: "Queue the project's one-shot index, or its retrospective ADR backfill",
|
|
31051
|
+
requestSchema: PROJECT_INDEX_BODY
|
|
31052
|
+
}),
|
|
31053
|
+
async (req, reply) => {
|
|
31054
|
+
const requestId = requestIdOf(req);
|
|
31055
|
+
try {
|
|
31056
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31057
|
+
if (!actor) return reply;
|
|
31058
|
+
const { projectId } = req.params;
|
|
31059
|
+
const { kind } = req.body ?? {};
|
|
31060
|
+
const queued = await commands.requestProjectIndex(actor, { projectId, kind: kind ?? "index", idempotencyKey: requiredIdempotencyKey(req) });
|
|
31061
|
+
return reply.status(202).send(queued);
|
|
31062
|
+
} catch (err) {
|
|
31063
|
+
return sendError(reply, err, requestId);
|
|
31064
|
+
}
|
|
31065
|
+
}
|
|
31066
|
+
);
|
|
31067
|
+
app.post(
|
|
31068
|
+
"/workers/registration-token",
|
|
31069
|
+
routeMeta({
|
|
31070
|
+
surface: "application",
|
|
31071
|
+
operationId: "worker.issueRegistrationToken",
|
|
31072
|
+
authPolicy: "application_actor",
|
|
31073
|
+
resourcePolicy: "account.manage",
|
|
31074
|
+
stability: "preview",
|
|
31075
|
+
summary: "Mint a single-use registration token for a worker of this account"
|
|
31076
|
+
}),
|
|
31077
|
+
async (req, reply) => {
|
|
31078
|
+
const requestId = requestIdOf(req);
|
|
31079
|
+
try {
|
|
31080
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31081
|
+
if (!actor) return reply;
|
|
31082
|
+
const issued = await commands.issueWorkerRegistrationToken(actor);
|
|
31083
|
+
return reply.status(201).send(issued);
|
|
31084
|
+
} catch (err) {
|
|
31085
|
+
return sendError(reply, err, requestId);
|
|
31086
|
+
}
|
|
31087
|
+
}
|
|
31088
|
+
);
|
|
31089
|
+
app.post(
|
|
31090
|
+
"/projects/:projectId/init-status",
|
|
31091
|
+
routeMeta({
|
|
31092
|
+
surface: "application",
|
|
31093
|
+
operationId: "project.reportInitStatus",
|
|
31094
|
+
authPolicy: "application_actor",
|
|
31095
|
+
resourcePolicy: "project.manage",
|
|
31096
|
+
stability: "preview",
|
|
31097
|
+
summary: "Report an init step the project's client took before it connected",
|
|
31098
|
+
requestSchema: INIT_STATUS_BODY
|
|
31099
|
+
}),
|
|
31100
|
+
async (req, reply) => {
|
|
31101
|
+
const requestId = requestIdOf(req);
|
|
31102
|
+
try {
|
|
31103
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31104
|
+
if (!actor) return reply;
|
|
31105
|
+
const { projectId } = req.params;
|
|
31106
|
+
const { phase, error } = req.body;
|
|
31107
|
+
return await commands.reportInitStatus(actor, { projectId, phase, error });
|
|
31108
|
+
} catch (err) {
|
|
31109
|
+
return sendError(reply, err, requestId);
|
|
31110
|
+
}
|
|
31111
|
+
}
|
|
31112
|
+
);
|
|
30965
31113
|
app.post(
|
|
30966
31114
|
"/projects/:projectId/repos",
|
|
30967
31115
|
routeMeta({
|
|
@@ -31766,7 +31914,7 @@ init_regimen_config();
|
|
|
31766
31914
|
// src/projects/cred-updates.ts
|
|
31767
31915
|
init_dist();
|
|
31768
31916
|
init_ws();
|
|
31769
|
-
import { nanoid as
|
|
31917
|
+
import { nanoid as nanoid32 } from "nanoid";
|
|
31770
31918
|
var CRED_KIND_NOT_REPLACEABLE = "cred_kind_not_replaceable";
|
|
31771
31919
|
function checkCredKind(kind) {
|
|
31772
31920
|
if (isCredKind(kind)) return { ok: true, kind };
|
|
@@ -31789,7 +31937,7 @@ async function retireNonReplaceableCredUpdates(db, log) {
|
|
|
31789
31937
|
return changes;
|
|
31790
31938
|
}
|
|
31791
31939
|
async function enqueueCredUpdate(db, projectId, kind, sealed, requestedBy, log) {
|
|
31792
|
-
const id = `cred_${
|
|
31940
|
+
const id = `cred_${nanoid32(16)}`;
|
|
31793
31941
|
const now = Date.now();
|
|
31794
31942
|
await db.run(
|
|
31795
31943
|
`UPDATE cred_updates SET state = 'rejected', detail = 'superseded by a newer replacement', updated_at = ?
|
|
@@ -31908,6 +32056,7 @@ function installCredUpdateHandlers(db, log) {
|
|
|
31908
32056
|
|
|
31909
32057
|
// src/application/read-model.ts
|
|
31910
32058
|
init_ws();
|
|
32059
|
+
init_farm2();
|
|
31911
32060
|
init_queue_view();
|
|
31912
32061
|
init_state_machine2();
|
|
31913
32062
|
init_delivery_decision();
|
|
@@ -32119,7 +32268,7 @@ function configProposalRow(stored) {
|
|
|
32119
32268
|
}
|
|
32120
32269
|
|
|
32121
32270
|
// src/config-proposals/state-machine.ts
|
|
32122
|
-
import { nanoid as
|
|
32271
|
+
import { nanoid as nanoid33 } from "nanoid";
|
|
32123
32272
|
function rowToProposal(raw) {
|
|
32124
32273
|
return {
|
|
32125
32274
|
id: raw["id"],
|
|
@@ -32144,7 +32293,7 @@ var PROPOSAL_COLUMNS = `id, project_id as projectId, user_input as userInput, st
|
|
|
32144
32293
|
created_at as createdAt, updated_at as updatedAt`;
|
|
32145
32294
|
var PROPOSAL_SELECT = `SELECT ${PROPOSAL_COLUMNS} FROM config_proposals`;
|
|
32146
32295
|
async function createProposal(db, projectId, userInput) {
|
|
32147
|
-
const id = `cfp_${
|
|
32296
|
+
const id = `cfp_${nanoid33(16)}`;
|
|
32148
32297
|
const now = Date.now();
|
|
32149
32298
|
await db.run(
|
|
32150
32299
|
`INSERT INTO config_proposals
|
|
@@ -32793,7 +32942,7 @@ async function projectDetailOf(db, projectId, raw) {
|
|
|
32793
32942
|
actionErrors: getProjectActions(projectId)?.actionErrors ?? []
|
|
32794
32943
|
};
|
|
32795
32944
|
}
|
|
32796
|
-
function createReadModel(db) {
|
|
32945
|
+
function createReadModel(db, options = {}) {
|
|
32797
32946
|
const SUBMISSION_COLUMNS = `${DRAFT_COLUMNS},
|
|
32798
32947
|
(SELECT COUNT(*) FROM draft_tasks c WHERE c.parent_draft_id = draft_tasks.id) as childCount,
|
|
32799
32948
|
EXISTS (SELECT 1 FROM tasks t
|
|
@@ -33485,6 +33634,35 @@ function createReadModel(db) {
|
|
|
33485
33634
|
const p = await getPresence(db, { email: viewer.email ?? "", userId: viewer.subjectId });
|
|
33486
33635
|
return presenceRowOf(p, new Set(viewer.projectIds));
|
|
33487
33636
|
},
|
|
33637
|
+
// --- the account's fleet (DEV-927) ---
|
|
33638
|
+
async listFarms(viewer) {
|
|
33639
|
+
const isOperator = viewer.email !== null && (options.isOperator?.(viewer.email) ?? false);
|
|
33640
|
+
return await listFarms(db, { email: viewer.email ?? "", userId: viewer.subjectId, isSuperAdmin: isOperator });
|
|
33641
|
+
},
|
|
33642
|
+
async listProjectDeliverables(projectId) {
|
|
33643
|
+
const rows = await db.all(
|
|
33644
|
+
`SELECT id, tracker_issue_key AS "taskKey", title, phase, updated_at AS "updatedAt"
|
|
33645
|
+
FROM tasks
|
|
33646
|
+
WHERE project_id = ? AND (archived IS NULL OR archived = 0)
|
|
33647
|
+
AND phase IN ('TRACKING', 'AWAITING_CLUSTER', 'DONE', 'NEEDS_MANUAL')
|
|
33648
|
+
ORDER BY CASE WHEN phase IN ('TRACKING', 'AWAITING_CLUSTER') THEN 0 ELSE 1 END,
|
|
33649
|
+
updated_at DESC
|
|
33650
|
+
LIMIT 50`,
|
|
33651
|
+
projectId
|
|
33652
|
+
);
|
|
33653
|
+
const out = [];
|
|
33654
|
+
for (const r of rows) {
|
|
33655
|
+
const byRepo = await loadTaskBranchesByRepo(db, r.id);
|
|
33656
|
+
const repos = Object.entries(byRepo).map(([repoName, b]) => ({
|
|
33657
|
+
repoName,
|
|
33658
|
+
branchName: b.branchName,
|
|
33659
|
+
baseRef: b.baseRef
|
|
33660
|
+
}));
|
|
33661
|
+
if (repos.length === 0) continue;
|
|
33662
|
+
out.push({ taskKey: r.taskKey, title: r.title, phase: r.phase, updatedAt: Number(r.updatedAt), repos });
|
|
33663
|
+
}
|
|
33664
|
+
return out;
|
|
33665
|
+
},
|
|
33488
33666
|
async readNotificationSettings(subjectId) {
|
|
33489
33667
|
const user = await db.get(`SELECT 1 as hit FROM users WHERE id = ?`, subjectId);
|
|
33490
33668
|
if (!user) return null;
|
|
@@ -33573,7 +33751,7 @@ init_state_machine();
|
|
|
33573
33751
|
// src/drafts/publish.ts
|
|
33574
33752
|
init_ws();
|
|
33575
33753
|
init_state_machine();
|
|
33576
|
-
import { nanoid as
|
|
33754
|
+
import { nanoid as nanoid34 } from "nanoid";
|
|
33577
33755
|
function firstNonBlank(...vals) {
|
|
33578
33756
|
for (const v of vals) {
|
|
33579
33757
|
if (v != null && v.trim() !== "") return v;
|
|
@@ -33581,7 +33759,7 @@ function firstNonBlank(...vals) {
|
|
|
33581
33759
|
return "";
|
|
33582
33760
|
}
|
|
33583
33761
|
async function dispatchPublish(db, draft, targetStatus, log) {
|
|
33584
|
-
const publishActionId = `dpub_${
|
|
33762
|
+
const publishActionId = `dpub_${nanoid34(16)}`;
|
|
33585
33763
|
const title = firstNonBlank(draft.editedTitle, draft.generatedTitle);
|
|
33586
33764
|
const body = firstNonBlank(draft.editedBody, draft.generatedBody);
|
|
33587
33765
|
const project = await db.get(`SELECT youtrack_project_key as youtrackProjectKey FROM projects WHERE id = ?`, draft.projectId);
|
|
@@ -33727,7 +33905,7 @@ async function maybeAdvancePackage(db, parentDraftId, log) {
|
|
|
33727
33905
|
log.info({ draftId: parentDraftId }, "package published (no links) \u2192 DONE");
|
|
33728
33906
|
return;
|
|
33729
33907
|
}
|
|
33730
|
-
const linkActionId = `dpkg_${
|
|
33908
|
+
const linkActionId = `dpkg_${nanoid34(16)}`;
|
|
33731
33909
|
const action = {
|
|
33732
33910
|
type: "apply_tracker_links",
|
|
33733
33911
|
draftId: parentDraftId,
|
|
@@ -33922,7 +34100,115 @@ async function discardDraft(db, draft, log) {
|
|
|
33922
34100
|
init_dist5();
|
|
33923
34101
|
init_regimen_write();
|
|
33924
34102
|
init_ws();
|
|
33925
|
-
|
|
34103
|
+
|
|
34104
|
+
// src/farm/provision.ts
|
|
34105
|
+
init_ws();
|
|
34106
|
+
init_pool_spec();
|
|
34107
|
+
import { nanoid as nanoid35 } from "nanoid";
|
|
34108
|
+
var pending3 = /* @__PURE__ */ new Map();
|
|
34109
|
+
var RESULT_TYPES = /* @__PURE__ */ new Set(["provision_result", "delete_result", "respawn_result"]);
|
|
34110
|
+
function handleFarmResult(msg) {
|
|
34111
|
+
const type = msg["type"];
|
|
34112
|
+
if (typeof type !== "string" || !RESULT_TYPES.has(type)) return;
|
|
34113
|
+
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
34114
|
+
const p = pending3.get(requestId);
|
|
34115
|
+
if (!p) return;
|
|
34116
|
+
clearTimeout(p.timer);
|
|
34117
|
+
pending3.delete(requestId);
|
|
34118
|
+
p.resolve(msg);
|
|
34119
|
+
}
|
|
34120
|
+
function awaitResult(requestId, send, timeoutMs) {
|
|
34121
|
+
if (!send()) return Promise.reject(new Error("farm_offline"));
|
|
34122
|
+
return new Promise((resolve4, reject) => {
|
|
34123
|
+
const timer = setTimeout(() => {
|
|
34124
|
+
pending3.delete(requestId);
|
|
34125
|
+
reject(new Error("farm_timeout"));
|
|
34126
|
+
}, timeoutMs);
|
|
34127
|
+
pending3.set(requestId, { resolve: resolve4, reject, timer });
|
|
34128
|
+
});
|
|
34129
|
+
}
|
|
34130
|
+
function requestProvision(farmId, projectId, sealedBundle, timeoutMs) {
|
|
34131
|
+
const requestId = nanoid35(16);
|
|
34132
|
+
return awaitResult(
|
|
34133
|
+
requestId,
|
|
34134
|
+
() => sendToFarm(farmId, { type: "provision_project", requestId, projectId, sealedBundle }),
|
|
34135
|
+
timeoutMs
|
|
34136
|
+
);
|
|
34137
|
+
}
|
|
34138
|
+
function requestDelete(farmId, projectId, timeoutMs) {
|
|
34139
|
+
const requestId = nanoid35(16);
|
|
34140
|
+
return awaitResult(
|
|
34141
|
+
requestId,
|
|
34142
|
+
() => sendToFarm(farmId, { type: "delete_project", requestId, projectId }),
|
|
34143
|
+
timeoutMs
|
|
34144
|
+
);
|
|
34145
|
+
}
|
|
34146
|
+
function requestRespawn(farmId, projectId, timeoutMs) {
|
|
34147
|
+
const requestId = nanoid35(16);
|
|
34148
|
+
return awaitResult(
|
|
34149
|
+
requestId,
|
|
34150
|
+
() => sendToFarm(farmId, { type: "respawn_project", requestId, projectId }),
|
|
34151
|
+
timeoutMs
|
|
34152
|
+
);
|
|
34153
|
+
}
|
|
34154
|
+
async function requestFarmPoolSync(db, farmId) {
|
|
34155
|
+
const accounts = await computeFarmPoolSpec(db, farmId);
|
|
34156
|
+
sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
|
|
34157
|
+
}
|
|
34158
|
+
|
|
34159
|
+
// src/runner/provision.ts
|
|
34160
|
+
init_ws();
|
|
34161
|
+
import { nanoid as nanoid36 } from "nanoid";
|
|
34162
|
+
var pending4 = /* @__PURE__ */ new Map();
|
|
34163
|
+
var RESULT_TYPES2 = /* @__PURE__ */ new Set(["provision_gh_runners_result", "delete_gh_runners_result"]);
|
|
34164
|
+
function handleRunnerResult(msg) {
|
|
34165
|
+
const type = msg["type"];
|
|
34166
|
+
if (typeof type !== "string" || !RESULT_TYPES2.has(type)) return;
|
|
34167
|
+
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
34168
|
+
const p = pending4.get(requestId);
|
|
34169
|
+
if (!p) return;
|
|
34170
|
+
clearTimeout(p.timer);
|
|
34171
|
+
pending4.delete(requestId);
|
|
34172
|
+
p.resolve(msg);
|
|
34173
|
+
}
|
|
34174
|
+
function awaitResult2(requestId, send, timeoutMs) {
|
|
34175
|
+
if (!send()) return Promise.reject(new Error("runner_offline"));
|
|
34176
|
+
return new Promise((resolve4, reject) => {
|
|
34177
|
+
const timer = setTimeout(() => {
|
|
34178
|
+
pending4.delete(requestId);
|
|
34179
|
+
reject(new Error("runner_timeout"));
|
|
34180
|
+
}, timeoutMs);
|
|
34181
|
+
pending4.set(requestId, { resolve: resolve4, reject, timer });
|
|
34182
|
+
});
|
|
34183
|
+
}
|
|
34184
|
+
function requestGhRunnersProvision(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
34185
|
+
const requestId = nanoid36(16);
|
|
34186
|
+
return awaitResult2(
|
|
34187
|
+
requestId,
|
|
34188
|
+
() => sendToRunner(runnerId, { type: "provision_gh_runners", requestId, projectId, sealedBundle }),
|
|
34189
|
+
timeoutMs
|
|
34190
|
+
);
|
|
34191
|
+
}
|
|
34192
|
+
function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
34193
|
+
const requestId = nanoid36(16);
|
|
34194
|
+
return awaitResult2(
|
|
34195
|
+
requestId,
|
|
34196
|
+
() => sendToRunner(runnerId, {
|
|
34197
|
+
type: "delete_gh_runners",
|
|
34198
|
+
requestId,
|
|
34199
|
+
projectId,
|
|
34200
|
+
...sealedBundle ? { sealedBundle } : {}
|
|
34201
|
+
}),
|
|
34202
|
+
timeoutMs
|
|
34203
|
+
);
|
|
34204
|
+
}
|
|
34205
|
+
|
|
34206
|
+
// src/application/write-model.ts
|
|
34207
|
+
init_farm2();
|
|
34208
|
+
init_runner2();
|
|
34209
|
+
init_init_status();
|
|
34210
|
+
init_sandbox_reg_tokens();
|
|
34211
|
+
init_abuse_throttle();
|
|
33926
34212
|
|
|
33927
34213
|
// src/projects/forget.ts
|
|
33928
34214
|
init_legacy_cred_bundles();
|
|
@@ -33987,8 +34273,71 @@ async function sweepOrphanedProjectRows(db) {
|
|
|
33987
34273
|
return removed;
|
|
33988
34274
|
}
|
|
33989
34275
|
|
|
34276
|
+
// src/runner/bindings.ts
|
|
34277
|
+
function parseGhRunnerBindings(json) {
|
|
34278
|
+
if (!json) return [];
|
|
34279
|
+
try {
|
|
34280
|
+
const parsed = JSON.parse(json);
|
|
34281
|
+
return Array.isArray(parsed.bindings) ? parsed.bindings : [];
|
|
34282
|
+
} catch {
|
|
34283
|
+
return [];
|
|
34284
|
+
}
|
|
34285
|
+
}
|
|
34286
|
+
function serialize(bindings) {
|
|
34287
|
+
return JSON.stringify({ bindings });
|
|
34288
|
+
}
|
|
34289
|
+
async function upsertGhRunnerBinding(db, projectId, binding) {
|
|
34290
|
+
await db.tx(async (tx) => {
|
|
34291
|
+
const row = await tx.get(
|
|
34292
|
+
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
34293
|
+
projectId
|
|
34294
|
+
);
|
|
34295
|
+
if (!row) return;
|
|
34296
|
+
const bindings = parseGhRunnerBindings(row.bindingsJson).filter(
|
|
34297
|
+
(b) => !(b.owner === binding.owner && b.repo === binding.repo)
|
|
34298
|
+
);
|
|
34299
|
+
bindings.push(binding);
|
|
34300
|
+
await tx.run(
|
|
34301
|
+
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
34302
|
+
serialize(bindings),
|
|
34303
|
+
projectId
|
|
34304
|
+
);
|
|
34305
|
+
});
|
|
34306
|
+
}
|
|
34307
|
+
async function removeGhRunnerBinding(db, projectId, owner, repo) {
|
|
34308
|
+
return db.tx(async (tx) => {
|
|
34309
|
+
const row = await tx.get(
|
|
34310
|
+
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
34311
|
+
projectId
|
|
34312
|
+
);
|
|
34313
|
+
if (!row) return false;
|
|
34314
|
+
const bindings = parseGhRunnerBindings(row.bindingsJson);
|
|
34315
|
+
const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
|
|
34316
|
+
if (kept.length === bindings.length) return false;
|
|
34317
|
+
await tx.run(
|
|
34318
|
+
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
34319
|
+
serialize(kept),
|
|
34320
|
+
projectId
|
|
34321
|
+
);
|
|
34322
|
+
return true;
|
|
34323
|
+
});
|
|
34324
|
+
}
|
|
34325
|
+
async function runnerBoundProjectCount(db, runnerId) {
|
|
34326
|
+
const rows = await db.all(
|
|
34327
|
+
`SELECT gh_runner_bindings_json AS json FROM projects WHERE gh_runner_bindings_json IS NOT NULL`
|
|
34328
|
+
);
|
|
34329
|
+
let n = 0;
|
|
34330
|
+
for (const r of rows) {
|
|
34331
|
+
try {
|
|
34332
|
+
const bindings = JSON.parse(r.json).bindings ?? [];
|
|
34333
|
+
if (bindings.some((b) => b.runnerId === runnerId)) n++;
|
|
34334
|
+
} catch {
|
|
34335
|
+
}
|
|
34336
|
+
}
|
|
34337
|
+
return n;
|
|
34338
|
+
}
|
|
34339
|
+
|
|
33990
34340
|
// src/application/write-model.ts
|
|
33991
|
-
init_bindings();
|
|
33992
34341
|
init_tracking_commands();
|
|
33993
34342
|
|
|
33994
34343
|
// src/config-proposals/apply.ts
|
|
@@ -34122,10 +34471,27 @@ function conflict(reason) {
|
|
|
34122
34471
|
function stale() {
|
|
34123
34472
|
return { ok: false, refusal: "stale_revision" };
|
|
34124
34473
|
}
|
|
34474
|
+
function notFound() {
|
|
34475
|
+
return { ok: false, refusal: "not_found" };
|
|
34476
|
+
}
|
|
34477
|
+
var FARM_PROVISION_TIMEOUT_MS = 6e4;
|
|
34478
|
+
var GH_RUNNER_RELAY_TIMEOUT_MS = 12e4;
|
|
34479
|
+
function nodeUnavailable(capability, err) {
|
|
34480
|
+
const reason = err instanceof Error ? err.message : `${capability}_error`;
|
|
34481
|
+
const what = capability === "farm" ? "The farm" : "The runner host";
|
|
34482
|
+
return {
|
|
34483
|
+
ok: false,
|
|
34484
|
+
refusal: "capability_unavailable",
|
|
34485
|
+
capability,
|
|
34486
|
+
message: reason === `${capability}_timeout` ? `${what} did not answer in time. Try again once it is responsive.` : reason === `${capability}_offline` ? `${what} is offline. Start it and try again.` : `${what} could not be reached for this. Try again in a moment.`,
|
|
34487
|
+
retryable: true
|
|
34488
|
+
};
|
|
34489
|
+
}
|
|
34125
34490
|
function taskState(row) {
|
|
34126
34491
|
return { ...row, revision: String(row.updatedAt) };
|
|
34127
34492
|
}
|
|
34128
|
-
|
|
34493
|
+
var WORKER_REG_TOKEN_MINTS = createAbuseThrottle({ windowMs: 60 * 6e4, max: 30 });
|
|
34494
|
+
function createWriteModel(db, log, hooks = {}) {
|
|
34129
34495
|
async function readTaskRow(projectId, key) {
|
|
34130
34496
|
const row = await db.get(
|
|
34131
34497
|
`SELECT project_id as projectId, tracker_issue_key as key, phase,
|
|
@@ -34200,6 +34566,13 @@ function createWriteModel(db, log) {
|
|
|
34200
34566
|
if (sandboxId !== null) sendToSandbox(sandboxId, { type: "pause_job", jobId: stored.sourceJobId });
|
|
34201
34567
|
await cancelActiveJobs(db, `config::${stored.id}`);
|
|
34202
34568
|
}
|
|
34569
|
+
function isOperator(actor) {
|
|
34570
|
+
return actor.email !== null && (hooks.isOperator?.(actor.email) ?? false);
|
|
34571
|
+
}
|
|
34572
|
+
async function ghRunnerHost(runnerId, actor) {
|
|
34573
|
+
const owns = isOperator(actor) ? await db.get(`SELECT id FROM runners WHERE id = ?`, runnerId) !== void 0 : await viewerOwnsRunner(db, { email: actor.email ?? "", userId: actor.subjectId }, runnerId);
|
|
34574
|
+
return owns ? ok(true) : notFound();
|
|
34575
|
+
}
|
|
34203
34576
|
return {
|
|
34204
34577
|
async readTask(projectId, taskKey) {
|
|
34205
34578
|
const row = await readTaskRow(projectId, taskKey);
|
|
@@ -34671,6 +35044,153 @@ function createWriteModel(db, log) {
|
|
|
34671
35044
|
* daemon behind it. The Context Repo remote is not touched: what the
|
|
34672
35045
|
* pipeline wrote about the work outlives the record of the project.
|
|
34673
35046
|
*/
|
|
35047
|
+
// --- registration and the account's fleet (DEV-927) ---
|
|
35048
|
+
async createProject({ project, owner }) {
|
|
35049
|
+
const trackerProvider = project.trackerProvider;
|
|
35050
|
+
const inserted = await db.tx(async (tx) => {
|
|
35051
|
+
const existing = await tx.get(`SELECT id FROM projects WHERE id = ?`, project.projectId);
|
|
35052
|
+
if (existing) return conflict("project_already_exists");
|
|
35053
|
+
const now = Date.now();
|
|
35054
|
+
await tx.run(
|
|
35055
|
+
`INSERT INTO projects (id, name, automation_level, youtrack_project_key, tracker_provider,
|
|
35056
|
+
repos_json, owner_email, owner_user_id, language_json,
|
|
35057
|
+
provisioning_status, context_repo_json, provisioned_at, provisioning_error, created_at)
|
|
35058
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'provisioned', ?, ?, NULL, ?)`,
|
|
35059
|
+
project.projectId,
|
|
35060
|
+
project.name,
|
|
35061
|
+
project.automationLevel,
|
|
35062
|
+
project.trackerProjectKey,
|
|
35063
|
+
trackerProvider,
|
|
35064
|
+
JSON.stringify(
|
|
35065
|
+
project.repos.map((r) => ({
|
|
35066
|
+
name: r.name,
|
|
35067
|
+
host: r.host,
|
|
35068
|
+
owner: r.owner,
|
|
35069
|
+
repoName: r.repoName,
|
|
35070
|
+
...r.defaultBranch ? { defaultBranch: r.defaultBranch } : {},
|
|
35071
|
+
...r.mountAs ? { mountAs: r.mountAs } : {},
|
|
35072
|
+
...r.mirror ? { mirror: { host: r.mirror.host, owner: r.mirror.owner, repoName: r.mirror.repoName } } : {}
|
|
35073
|
+
}))
|
|
35074
|
+
),
|
|
35075
|
+
owner.email,
|
|
35076
|
+
owner.subjectId,
|
|
35077
|
+
project.language === null ? null : JSON.stringify(project.language),
|
|
35078
|
+
JSON.stringify(project.contextRepo),
|
|
35079
|
+
now,
|
|
35080
|
+
now
|
|
35081
|
+
);
|
|
35082
|
+
return ok({ projectId: project.projectId });
|
|
35083
|
+
});
|
|
35084
|
+
if (!inserted.ok) return inserted;
|
|
35085
|
+
await beginInitTracking(db, project.projectId, "registered");
|
|
35086
|
+
log.info({ projectId: project.projectId, contextRepo: project.contextRepo }, "project registered, Context Repo provisioned by the client");
|
|
35087
|
+
hooks.projectRegistered?.({
|
|
35088
|
+
projectId: project.projectId,
|
|
35089
|
+
name: project.name,
|
|
35090
|
+
automationLevel: project.automationLevel,
|
|
35091
|
+
ownerEmail: owner.email,
|
|
35092
|
+
repoCount: project.repos.length
|
|
35093
|
+
});
|
|
35094
|
+
return inserted;
|
|
35095
|
+
},
|
|
35096
|
+
async provisionOnFarm({ farmId, projectId, sealedBundle, actor }) {
|
|
35097
|
+
const usable = isOperator(actor) ? await db.get(`SELECT id FROM farms WHERE id = ?`, farmId) !== void 0 : await viewerOwnsFarm(db, { email: actor.email ?? "", userId: actor.subjectId }, farmId);
|
|
35098
|
+
if (!usable) return notFound();
|
|
35099
|
+
await beginInitTracking(db, projectId, "farm_provisioning");
|
|
35100
|
+
let result;
|
|
35101
|
+
try {
|
|
35102
|
+
result = await requestProvision(farmId, projectId, sealedBundle, FARM_PROVISION_TIMEOUT_MS);
|
|
35103
|
+
} catch (err) {
|
|
35104
|
+
const reason = err instanceof Error ? err.message : "provision_failed";
|
|
35105
|
+
await recordInitError(
|
|
35106
|
+
db,
|
|
35107
|
+
projectId,
|
|
35108
|
+
reason === "farm_offline" ? "the hosting farm is offline" : reason === "farm_timeout" ? "the hosting farm did not respond in time" : `farm provisioning failed: ${reason}`
|
|
35109
|
+
);
|
|
35110
|
+
return nodeUnavailable("farm", err);
|
|
35111
|
+
}
|
|
35112
|
+
if (result.ok) {
|
|
35113
|
+
await db.run(`UPDATE projects SET farm_id = ? WHERE id = ?`, farmId, projectId);
|
|
35114
|
+
await advanceInitPhase(db, projectId, "daemon_starting");
|
|
35115
|
+
requestFarmPoolSync(db, farmId).catch(
|
|
35116
|
+
(err) => log.warn({ farmId, err }, "farm pool sync after provision failed")
|
|
35117
|
+
);
|
|
35118
|
+
} else {
|
|
35119
|
+
await recordInitError(db, projectId, `farm provisioning failed: ${result.error ?? "unknown error"}`);
|
|
35120
|
+
}
|
|
35121
|
+
return ok({ ok: result.ok, daemonStatus: result.daemonStatus ?? null, error: result.error ?? null });
|
|
35122
|
+
},
|
|
35123
|
+
async provisionGhRunners({ runnerId, projectId, sealedBundle, actor }) {
|
|
35124
|
+
const guard = await ghRunnerHost(runnerId, actor);
|
|
35125
|
+
if (!guard.ok) return guard;
|
|
35126
|
+
let result;
|
|
35127
|
+
try {
|
|
35128
|
+
result = await requestGhRunnersProvision(runnerId, projectId, sealedBundle, GH_RUNNER_RELAY_TIMEOUT_MS);
|
|
35129
|
+
} catch (err) {
|
|
35130
|
+
return nodeUnavailable("runner", err);
|
|
35131
|
+
}
|
|
35132
|
+
const runners = result.runners ?? [];
|
|
35133
|
+
if (result.ok) {
|
|
35134
|
+
const connectedAt = Date.now();
|
|
35135
|
+
for (const r of runners) {
|
|
35136
|
+
await upsertGhRunnerBinding(db, projectId, {
|
|
35137
|
+
owner: r.owner,
|
|
35138
|
+
repo: r.repo,
|
|
35139
|
+
runnerId,
|
|
35140
|
+
connectedAt,
|
|
35141
|
+
status: r.status,
|
|
35142
|
+
...r.error ? { error: r.error } : {},
|
|
35143
|
+
...r.labels ? { labels: r.labels } : {}
|
|
35144
|
+
});
|
|
35145
|
+
}
|
|
35146
|
+
}
|
|
35147
|
+
return ok({
|
|
35148
|
+
ok: result.ok,
|
|
35149
|
+
runners: runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error ?? null, labels: r.labels ?? [] })),
|
|
35150
|
+
error: result.error ?? null
|
|
35151
|
+
});
|
|
35152
|
+
},
|
|
35153
|
+
async teardownGhRunners({ runnerId, projectId, sealedBundle, repo, actor }) {
|
|
35154
|
+
const guard = await ghRunnerHost(runnerId, actor);
|
|
35155
|
+
if (!guard.ok) return guard;
|
|
35156
|
+
let result;
|
|
35157
|
+
try {
|
|
35158
|
+
result = await requestGhRunnersDelete(runnerId, projectId, sealedBundle ?? void 0, GH_RUNNER_RELAY_TIMEOUT_MS);
|
|
35159
|
+
} catch (err) {
|
|
35160
|
+
return nodeUnavailable("runner", err);
|
|
35161
|
+
}
|
|
35162
|
+
if (result.ok && repo !== null) {
|
|
35163
|
+
await removeGhRunnerBinding(db, projectId, repo.owner, repo.repo);
|
|
35164
|
+
}
|
|
35165
|
+
return ok({ ok: result.ok, error: result.error ?? null });
|
|
35166
|
+
},
|
|
35167
|
+
async enqueueProjectIndex({ projectId, kind }) {
|
|
35168
|
+
const p = await db.get(`SELECT id, context_repo_json as contextRepoJson FROM projects WHERE id = ?`, projectId);
|
|
35169
|
+
if (!p) return notFound();
|
|
35170
|
+
if (!p.contextRepoJson) return conflict("context_repo_not_provisioned");
|
|
35171
|
+
const jobId = kind === "backfill" ? await enqueueBackfillJob(db, projectId) : await enqueueIndexJob(db, projectId);
|
|
35172
|
+
await advanceInitPhase(db, projectId, "indexing");
|
|
35173
|
+
log.info({ projectId, jobId, kind }, kind === "backfill" ? "backfill job enqueued" : "index job enqueued");
|
|
35174
|
+
return ok({ jobId });
|
|
35175
|
+
},
|
|
35176
|
+
async issueWorkerRegistrationToken({ owner }) {
|
|
35177
|
+
if (owner.email === null) return { ok: false, refusal: "unprocessable", reason: "account_has_no_email" };
|
|
35178
|
+
if (!WORKER_REG_TOKEN_MINTS.take(`email:${owner.email}`)) {
|
|
35179
|
+
log.warn({ email: owner.email }, "ephemeral sandbox reg token mint throttled");
|
|
35180
|
+
return { ok: false, refusal: "rate_limited" };
|
|
35181
|
+
}
|
|
35182
|
+
const issued = await issueEphemeralSandboxRegToken(db, owner.email);
|
|
35183
|
+
log.info({ email: owner.email }, "ephemeral sandbox reg token issued for a client");
|
|
35184
|
+
return ok(issued);
|
|
35185
|
+
},
|
|
35186
|
+
async reportInitStatus({ projectId, phase, error }) {
|
|
35187
|
+
const recorded = await advanceInitPhase(db, projectId, phase);
|
|
35188
|
+
if (error !== null) {
|
|
35189
|
+
const cur = await getInitStatus(db, projectId);
|
|
35190
|
+
if (cur && cur.phase === phase) await recordInitError(db, projectId, error);
|
|
35191
|
+
}
|
|
35192
|
+
return ok({ recorded });
|
|
35193
|
+
},
|
|
34674
35194
|
async deleteProject({ projectId }) {
|
|
34675
35195
|
const row = await db.get(`SELECT farm_id as farmId FROM projects WHERE id = ?`, projectId);
|
|
34676
35196
|
if (!row) return conflict("project_not_found");
|
|
@@ -36884,8 +37404,6 @@ init_settings();
|
|
|
36884
37404
|
init_users();
|
|
36885
37405
|
init_ws();
|
|
36886
37406
|
init_sandbox2();
|
|
36887
|
-
init_provision();
|
|
36888
|
-
init_bindings();
|
|
36889
37407
|
async function adminConsoleRoutes(app, deps) {
|
|
36890
37408
|
const { db, config, mailer, logFile } = deps;
|
|
36891
37409
|
app.get("/api/admin/users", async (req, reply) => {
|
|
@@ -39079,10 +39597,18 @@ async function negotiateJobCredentials(db, spec, workerId, log, issuer) {
|
|
|
39079
39597
|
// src/routes/jobs.ts
|
|
39080
39598
|
init_sandbox2();
|
|
39081
39599
|
init_ws();
|
|
39600
|
+
init_route_registry();
|
|
39082
39601
|
async function jobsRoutes(app, deps) {
|
|
39083
39602
|
const { db, config } = deps;
|
|
39084
39603
|
const notifyDeps = deps.notify;
|
|
39085
|
-
app.post("/jobs/claim",
|
|
39604
|
+
app.post("/node/v1/jobs/claim", routeMeta({
|
|
39605
|
+
surface: "node",
|
|
39606
|
+
operationId: "node.jobs.claim",
|
|
39607
|
+
authPolicy: "node_actor",
|
|
39608
|
+
resourcePolicy: "node.connect",
|
|
39609
|
+
stability: "preview",
|
|
39610
|
+
summary: "Claim the next job for this SANDBOX (long poll)"
|
|
39611
|
+
}), async (req, reply) => {
|
|
39086
39612
|
const sandbox = await authenticateSandboxByBearer(db, req.headers.authorization);
|
|
39087
39613
|
if (!sandbox) {
|
|
39088
39614
|
return reply.status(401).send({ error: "unauthorized" });
|
|
@@ -39136,8 +39662,16 @@ async function jobsRoutes(app, deps) {
|
|
|
39136
39662
|
}
|
|
39137
39663
|
});
|
|
39138
39664
|
app.post(
|
|
39139
|
-
"/jobs/heartbeat",
|
|
39665
|
+
"/node/v1/jobs/heartbeat",
|
|
39140
39666
|
{
|
|
39667
|
+
...routeMeta({
|
|
39668
|
+
surface: "node",
|
|
39669
|
+
operationId: "node.jobs.heartbeat",
|
|
39670
|
+
authPolicy: "node_actor",
|
|
39671
|
+
resourcePolicy: "node.connect",
|
|
39672
|
+
stability: "preview",
|
|
39673
|
+
summary: "Keep a claimed job's lease alive"
|
|
39674
|
+
}),
|
|
39141
39675
|
schema: {
|
|
39142
39676
|
body: {
|
|
39143
39677
|
type: "object",
|
|
@@ -39199,8 +39733,16 @@ async function jobsRoutes(app, deps) {
|
|
|
39199
39733
|
}
|
|
39200
39734
|
);
|
|
39201
39735
|
app.post(
|
|
39202
|
-
"/jobs/release",
|
|
39736
|
+
"/node/v1/jobs/release",
|
|
39203
39737
|
{
|
|
39738
|
+
...routeMeta({
|
|
39739
|
+
surface: "node",
|
|
39740
|
+
operationId: "node.jobs.release",
|
|
39741
|
+
authPolicy: "node_actor",
|
|
39742
|
+
resourcePolicy: "node.connect",
|
|
39743
|
+
stability: "preview",
|
|
39744
|
+
summary: "Hand a claimed job back to the queue"
|
|
39745
|
+
}),
|
|
39204
39746
|
schema: {
|
|
39205
39747
|
body: {
|
|
39206
39748
|
type: "object",
|
|
@@ -39236,8 +39778,16 @@ async function jobsRoutes(app, deps) {
|
|
|
39236
39778
|
}
|
|
39237
39779
|
);
|
|
39238
39780
|
app.post(
|
|
39239
|
-
"/jobs/result",
|
|
39781
|
+
"/node/v1/jobs/result",
|
|
39240
39782
|
{
|
|
39783
|
+
...routeMeta({
|
|
39784
|
+
surface: "node",
|
|
39785
|
+
operationId: "node.jobs.result",
|
|
39786
|
+
authPolicy: "node_actor",
|
|
39787
|
+
resourcePolicy: "node.connect",
|
|
39788
|
+
stability: "preview",
|
|
39789
|
+
summary: "Report a finished job"
|
|
39790
|
+
}),
|
|
39241
39791
|
schema: {
|
|
39242
39792
|
body: {
|
|
39243
39793
|
type: "object",
|
|
@@ -39299,9 +39849,7 @@ init_dist();
|
|
|
39299
39849
|
init_auth();
|
|
39300
39850
|
init_project_access();
|
|
39301
39851
|
init_users();
|
|
39302
|
-
init_cli_auth();
|
|
39303
39852
|
init_lifecycle();
|
|
39304
|
-
init_queue();
|
|
39305
39853
|
init_context_outbox();
|
|
39306
39854
|
init_init_status();
|
|
39307
39855
|
init_project_config();
|
|
@@ -39510,7 +40058,6 @@ function requestTrackerFields(projectId, projectKey, timeoutMs) {
|
|
|
39510
40058
|
|
|
39511
40059
|
// src/routes/projects.ts
|
|
39512
40060
|
init_ws();
|
|
39513
|
-
init_provision();
|
|
39514
40061
|
|
|
39515
40062
|
// src/farm/move.ts
|
|
39516
40063
|
init_ws();
|
|
@@ -39594,227 +40141,10 @@ function requestGhRunnerTeardown(projectId, runner, target, timeoutMs) {
|
|
|
39594
40141
|
}
|
|
39595
40142
|
|
|
39596
40143
|
// src/routes/projects.ts
|
|
39597
|
-
init_bindings();
|
|
39598
40144
|
init_farm2();
|
|
39599
40145
|
init_runner2();
|
|
39600
|
-
var createProjectBodySchema = {
|
|
39601
|
-
type: "object",
|
|
39602
|
-
required: ["projectId", "name", "automationLevel", "repos"],
|
|
39603
|
-
properties: {
|
|
39604
|
-
projectId: { type: "string" },
|
|
39605
|
-
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
39606
|
-
automationLevel: { type: "string", enum: ["assisted", "full"] },
|
|
39607
|
-
// ADR-0034 (DEV-456): tracker provider chosen at init; absent (older CLI)
|
|
39608
|
-
// = youtrack. For `internal` youtrackProjectKey carries the key prefix.
|
|
39609
|
-
trackerProvider: { type: "string", enum: ["internal", "youtrack"] },
|
|
39610
|
-
youtrackProjectKey: { type: "string" },
|
|
39611
|
-
// DEV-200: legacy owner-email field. Still accepted so an older CLI's
|
|
39612
|
-
// request validates, but ignored — ownership always comes from the CLI
|
|
39613
|
-
// token's identity (DEV-377).
|
|
39614
|
-
ownerEmail: { type: "string" },
|
|
39615
|
-
repos: {
|
|
39616
|
-
type: "array",
|
|
39617
|
-
minItems: 0,
|
|
39618
|
-
items: {
|
|
39619
|
-
type: "object",
|
|
39620
|
-
required: ["name", "host", "owner", "repoName"],
|
|
39621
|
-
properties: {
|
|
39622
|
-
name: { type: "string", minLength: 1 },
|
|
39623
|
-
host: { type: "string", enum: ["github", "bitbucket", "gitlab"] },
|
|
39624
|
-
owner: { type: "string", minLength: 1 },
|
|
39625
|
-
repoName: { type: "string", minLength: 1 },
|
|
39626
|
-
defaultBranch: { type: "string" },
|
|
39627
|
-
mountAs: { type: "string" }
|
|
39628
|
-
}
|
|
39629
|
-
}
|
|
39630
|
-
},
|
|
39631
|
-
// DEV-65 (ADR-0008): the CLI provisions the Context Repo and reports the
|
|
39632
|
-
// resolved location here. The HUB only records it — it never holds git
|
|
39633
|
-
// credentials. Absent only on the legacy HUB-provisioning path.
|
|
39634
|
-
contextRepo: {
|
|
39635
|
-
type: "object",
|
|
39636
|
-
required: ["host", "owner", "name", "cloneUrl", "branch"],
|
|
39637
|
-
properties: {
|
|
39638
|
-
// DEV-875: `local` = a bare repository on the CLI's machine, reported
|
|
39639
|
-
// with its filesystem path as `cloneUrl`. The HUB records it like any
|
|
39640
|
-
// other location — it has no git access to either kind.
|
|
39641
|
-
host: { type: "string", enum: ["github", "bitbucket", "gitlab", "local"] },
|
|
39642
|
-
owner: { type: "string", minLength: 1 },
|
|
39643
|
-
name: { type: "string", minLength: 1 },
|
|
39644
|
-
cloneUrl: { type: "string", minLength: 1 },
|
|
39645
|
-
branch: { type: "string", minLength: 1 }
|
|
39646
|
-
}
|
|
39647
|
-
}
|
|
39648
|
-
}
|
|
39649
|
-
};
|
|
39650
|
-
var repoSpecSchema = {
|
|
39651
|
-
type: "object",
|
|
39652
|
-
required: ["name", "host", "owner", "repoName"],
|
|
39653
|
-
additionalProperties: false,
|
|
39654
|
-
properties: {
|
|
39655
|
-
name: { type: "string", minLength: 1, maxLength: 100 },
|
|
39656
|
-
host: { type: "string", enum: ["github", "bitbucket", "gitlab"] },
|
|
39657
|
-
owner: { type: "string", minLength: 1 },
|
|
39658
|
-
repoName: { type: "string", minLength: 1 },
|
|
39659
|
-
defaultBranch: { type: "string" },
|
|
39660
|
-
mountAs: { type: "string" }
|
|
39661
|
-
}
|
|
39662
|
-
};
|
|
39663
|
-
async function resolveRepoMutator(db, config, req, reply, projectId) {
|
|
39664
|
-
const cliAuth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39665
|
-
if (cliAuth) {
|
|
39666
|
-
if (!await canActOnProject(db, config, cliAuth, projectId)) {
|
|
39667
|
-
reply.status(403).send({ error: "forbidden" });
|
|
39668
|
-
return null;
|
|
39669
|
-
}
|
|
39670
|
-
return { email: cliAuth.email, userId: cliAuth.userId, viaCli: true };
|
|
39671
|
-
}
|
|
39672
|
-
const session = await requireProjectAccess(db, config, req, reply, projectId);
|
|
39673
|
-
if (!session) return null;
|
|
39674
|
-
return { email: session.email, userId: session.userId, viaCli: false };
|
|
39675
|
-
}
|
|
39676
|
-
async function loadRow(db, projectId) {
|
|
39677
|
-
const row = await db.get(
|
|
39678
|
-
`SELECT id, repos_json as reposJson, context_repo_json as contextRepoJson,
|
|
39679
|
-
provisioning_status as provisioningStatus
|
|
39680
|
-
FROM projects WHERE id = ?`,
|
|
39681
|
-
projectId
|
|
39682
|
-
);
|
|
39683
|
-
return {
|
|
39684
|
-
projectId: row.id,
|
|
39685
|
-
provisioningStatus: row.provisioningStatus,
|
|
39686
|
-
...row.contextRepoJson ? { contextRepo: JSON.parse(row.contextRepoJson) } : {},
|
|
39687
|
-
repos: JSON.parse(row.reposJson)
|
|
39688
|
-
};
|
|
39689
|
-
}
|
|
39690
40146
|
async function projectsRoutes(app, deps) {
|
|
39691
|
-
const { db, config
|
|
39692
|
-
app.post(
|
|
39693
|
-
"/projects",
|
|
39694
|
-
{ schema: { body: createProjectBodySchema } },
|
|
39695
|
-
async (req, reply) => {
|
|
39696
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39697
|
-
if (!auth) {
|
|
39698
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
39699
|
-
}
|
|
39700
|
-
const body = req.body;
|
|
39701
|
-
if (!PROJECT_ID_RE.test(body.projectId)) {
|
|
39702
|
-
return reply.status(400).send({
|
|
39703
|
-
error: "invalid_project_id",
|
|
39704
|
-
hint: "lowercase letters, digits and dashes; starts with a letter; 2\u201331 chars"
|
|
39705
|
-
});
|
|
39706
|
-
}
|
|
39707
|
-
const existing = await db.get(`SELECT id FROM projects WHERE id = ?`, body.projectId);
|
|
39708
|
-
if (existing) {
|
|
39709
|
-
return reply.status(409).send({ error: "project_already_exists", projectId: body.projectId });
|
|
39710
|
-
}
|
|
39711
|
-
const now = Date.now();
|
|
39712
|
-
const ownerEmail = auth.email;
|
|
39713
|
-
const ownerUserId = auth.userId;
|
|
39714
|
-
const trackerProvider = body.trackerProvider === "internal" ? "internal" : "youtrack";
|
|
39715
|
-
await db.run(`INSERT INTO projects (id, name, automation_level, youtrack_project_key,
|
|
39716
|
-
tracker_provider, repos_json, owner_email, owner_user_id,
|
|
39717
|
-
provisioning_status, created_at)
|
|
39718
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`, body.projectId, body.name, body.automationLevel, body.youtrackProjectKey ?? null, trackerProvider, JSON.stringify(body.repos), ownerEmail, ownerUserId, now);
|
|
39719
|
-
if (!body.contextRepo) {
|
|
39720
|
-
await db.run(`DELETE FROM projects WHERE id = ?`, body.projectId);
|
|
39721
|
-
return reply.status(400).send({
|
|
39722
|
-
error: "context_repo_required",
|
|
39723
|
-
hint: "the CLI provisions the Context Repo and must send `contextRepo` (upgrade the CLI)"
|
|
39724
|
-
});
|
|
39725
|
-
}
|
|
39726
|
-
app.log.info(
|
|
39727
|
-
{ projectId: body.projectId, contextRepo: body.contextRepo },
|
|
39728
|
-
"project registered, Context Repo provisioned by CLI"
|
|
39729
|
-
);
|
|
39730
|
-
await db.run(`UPDATE projects
|
|
39731
|
-
SET provisioning_status = 'provisioned', context_repo_json = ?,
|
|
39732
|
-
provisioned_at = ?, provisioning_error = NULL
|
|
39733
|
-
WHERE id = ?`, JSON.stringify(body.contextRepo), Date.now(), body.projectId);
|
|
39734
|
-
await beginInitTracking(db, body.projectId, "registered");
|
|
39735
|
-
void mailer.sendProjectRegistered(config.registrationNotifyEmail, {
|
|
39736
|
-
projectName: body.name,
|
|
39737
|
-
projectId: body.projectId,
|
|
39738
|
-
automationLevel: body.automationLevel,
|
|
39739
|
-
ownerEmail: ownerEmail ?? void 0,
|
|
39740
|
-
repoCount: body.repos.length,
|
|
39741
|
-
projectUrl: `${config.baseUrl}/?project=${encodeURIComponent(body.projectId)}`
|
|
39742
|
-
}).catch(
|
|
39743
|
-
(err) => app.log.error(
|
|
39744
|
-
{ err, projectId: body.projectId },
|
|
39745
|
-
"failed to send project-registered notification"
|
|
39746
|
-
)
|
|
39747
|
-
);
|
|
39748
|
-
return reply.status(201).send(await loadRow(db, body.projectId));
|
|
39749
|
-
}
|
|
39750
|
-
);
|
|
39751
|
-
app.post(
|
|
39752
|
-
"/projects/:projectId/index",
|
|
39753
|
-
{
|
|
39754
|
-
schema: {
|
|
39755
|
-
body: {
|
|
39756
|
-
type: "object",
|
|
39757
|
-
properties: { reindex: { type: "boolean" }, backfill: { type: "boolean" } }
|
|
39758
|
-
}
|
|
39759
|
-
}
|
|
39760
|
-
},
|
|
39761
|
-
async (req, reply) => {
|
|
39762
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39763
|
-
if (!auth) {
|
|
39764
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
39765
|
-
}
|
|
39766
|
-
if (!await canActOnProject(db, config, auth, req.params.projectId)) {
|
|
39767
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
39768
|
-
}
|
|
39769
|
-
const p = await db.get(`SELECT id, context_repo_json as contextRepoJson FROM projects WHERE id = ?`, req.params.projectId);
|
|
39770
|
-
if (!p) return reply.status(404).send({ error: "project_not_found" });
|
|
39771
|
-
if (!p.contextRepoJson) {
|
|
39772
|
-
return reply.status(409).send({ error: "context_repo_not_provisioned" });
|
|
39773
|
-
}
|
|
39774
|
-
const backfill = req.body?.backfill === true;
|
|
39775
|
-
const jobId = backfill ? await enqueueBackfillJob(db, req.params.projectId) : await enqueueIndexJob(db, req.params.projectId);
|
|
39776
|
-
await advanceInitPhase(db, req.params.projectId, "indexing");
|
|
39777
|
-
app.log.info(
|
|
39778
|
-
{ projectId: req.params.projectId, jobId, reindex: req.body?.reindex === true, backfill },
|
|
39779
|
-
backfill ? "backfill job enqueued" : "index job enqueued"
|
|
39780
|
-
);
|
|
39781
|
-
return reply.status(202).send({ jobId });
|
|
39782
|
-
}
|
|
39783
|
-
);
|
|
39784
|
-
app.post(
|
|
39785
|
-
"/projects/:projectId/init-status",
|
|
39786
|
-
{
|
|
39787
|
-
schema: {
|
|
39788
|
-
body: {
|
|
39789
|
-
type: "object",
|
|
39790
|
-
required: ["phase"],
|
|
39791
|
-
properties: {
|
|
39792
|
-
phase: { type: "string", enum: ["context_cloning"] },
|
|
39793
|
-
error: { type: "string", minLength: 1, maxLength: 500 }
|
|
39794
|
-
}
|
|
39795
|
-
}
|
|
39796
|
-
}
|
|
39797
|
-
},
|
|
39798
|
-
async (req, reply) => {
|
|
39799
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39800
|
-
if (!auth) {
|
|
39801
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
39802
|
-
}
|
|
39803
|
-
if (!await canActOnProject(db, config, auth, req.params.projectId)) {
|
|
39804
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
39805
|
-
}
|
|
39806
|
-
const exists = await db.get(`SELECT id FROM projects WHERE id = ?`, req.params.projectId);
|
|
39807
|
-
if (!exists) return reply.status(404).send({ error: "project_not_found" });
|
|
39808
|
-
const advanced = await advanceInitPhase(db, req.params.projectId, req.body.phase);
|
|
39809
|
-
if (req.body.error) {
|
|
39810
|
-
const cur = await getInitStatus(db, req.params.projectId);
|
|
39811
|
-
if (cur && cur.phase === req.body.phase) {
|
|
39812
|
-
await recordInitError(db, req.params.projectId, req.body.error);
|
|
39813
|
-
}
|
|
39814
|
-
}
|
|
39815
|
-
return { recorded: advanced };
|
|
39816
|
-
}
|
|
39817
|
-
);
|
|
40147
|
+
const { db, config } = deps;
|
|
39818
40148
|
app.get(
|
|
39819
40149
|
"/projects/:projectId/pause",
|
|
39820
40150
|
async (req, reply) => {
|
|
@@ -40201,50 +40531,6 @@ async function projectsRoutes(app, deps) {
|
|
|
40201
40531
|
return { id, delivered };
|
|
40202
40532
|
}
|
|
40203
40533
|
);
|
|
40204
|
-
app.get(
|
|
40205
|
-
"/projects/:projectId/deliverables",
|
|
40206
|
-
async (req, reply) => {
|
|
40207
|
-
const projectId = req.params.projectId;
|
|
40208
|
-
const cliAuth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
40209
|
-
if (cliAuth) {
|
|
40210
|
-
if (!await canActOnProject(db, config, cliAuth, projectId)) {
|
|
40211
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
40212
|
-
}
|
|
40213
|
-
const exists = await db.get(`SELECT 1 FROM projects WHERE id = ?`, projectId);
|
|
40214
|
-
if (!exists) return reply.status(404).send({ error: "project not found" });
|
|
40215
|
-
} else if (!await requireProjectAccess(db, config, req, reply, projectId)) {
|
|
40216
|
-
return reply;
|
|
40217
|
-
}
|
|
40218
|
-
const rows = await db.all(
|
|
40219
|
-
`SELECT id, tracker_issue_key AS taskKey, title, phase, updated_at AS updatedAt
|
|
40220
|
-
FROM tasks
|
|
40221
|
-
WHERE project_id = ? AND (archived IS NULL OR archived = 0)
|
|
40222
|
-
AND phase IN ('TRACKING', 'AWAITING_CLUSTER', 'DONE', 'NEEDS_MANUAL')
|
|
40223
|
-
ORDER BY CASE WHEN phase IN ('TRACKING', 'AWAITING_CLUSTER') THEN 0 ELSE 1 END,
|
|
40224
|
-
updated_at DESC
|
|
40225
|
-
LIMIT 50`,
|
|
40226
|
-
projectId
|
|
40227
|
-
);
|
|
40228
|
-
const deliverables = [];
|
|
40229
|
-
for (const r of rows) {
|
|
40230
|
-
const byRepo = await loadTaskBranchesByRepo(db, r.id);
|
|
40231
|
-
const repos = Object.entries(byRepo).map(([repoName, b]) => ({
|
|
40232
|
-
repoName,
|
|
40233
|
-
branchName: b.branchName,
|
|
40234
|
-
baseRef: b.baseRef
|
|
40235
|
-
}));
|
|
40236
|
-
if (repos.length === 0) continue;
|
|
40237
|
-
deliverables.push({
|
|
40238
|
-
taskKey: r.taskKey,
|
|
40239
|
-
title: r.title,
|
|
40240
|
-
phase: r.phase,
|
|
40241
|
-
updatedAt: r.updatedAt,
|
|
40242
|
-
repos
|
|
40243
|
-
});
|
|
40244
|
-
}
|
|
40245
|
-
return { deliverables };
|
|
40246
|
-
}
|
|
40247
|
-
);
|
|
40248
40534
|
app.get(
|
|
40249
40535
|
"/projects/:projectId/prompts",
|
|
40250
40536
|
async (req, reply) => {
|
|
@@ -40662,88 +40948,6 @@ async function projectsRoutes(app, deps) {
|
|
|
40662
40948
|
return reply.status(202).send({ queued: true });
|
|
40663
40949
|
}
|
|
40664
40950
|
);
|
|
40665
|
-
app.post(
|
|
40666
|
-
"/projects/:projectId/repos",
|
|
40667
|
-
{ schema: { body: repoSpecSchema } },
|
|
40668
|
-
async (req, reply) => {
|
|
40669
|
-
const actor = await resolveRepoMutator(db, config, req, reply, req.params.projectId);
|
|
40670
|
-
if (!actor) return reply;
|
|
40671
|
-
const { projectId } = req.params;
|
|
40672
|
-
const repo = req.body;
|
|
40673
|
-
const mountAs = repo.mountAs ?? repo.name;
|
|
40674
|
-
const outcome = await db.tx(async (tx) => {
|
|
40675
|
-
const row = await tx.get(`SELECT repos_json as reposJson FROM projects WHERE id = ?`, projectId);
|
|
40676
|
-
if (!row) return { status: 404, error: "project_not_found" };
|
|
40677
|
-
const repos = JSON.parse(row.reposJson);
|
|
40678
|
-
if (!actor.viaCli && repos.length > 0 && !repos.some((r) => r.host === repo.host)) {
|
|
40679
|
-
return {
|
|
40680
|
-
status: 409,
|
|
40681
|
-
error: "unknown_host",
|
|
40682
|
-
hint: "add a repo on a new git host from the CLI (`codepipe repo add`), where its token is entered locally"
|
|
40683
|
-
};
|
|
40684
|
-
}
|
|
40685
|
-
if (repos.some((r) => r.name === repo.name)) return { status: 409, error: "duplicate_repo_name" };
|
|
40686
|
-
if (repos.some((r) => (r.mountAs ?? r.name) === mountAs)) {
|
|
40687
|
-
return { status: 409, error: "duplicate_mount_as" };
|
|
40688
|
-
}
|
|
40689
|
-
const added = {
|
|
40690
|
-
name: repo.name,
|
|
40691
|
-
host: repo.host,
|
|
40692
|
-
owner: repo.owner,
|
|
40693
|
-
repoName: repo.repoName,
|
|
40694
|
-
...repo.defaultBranch ? { defaultBranch: repo.defaultBranch } : {},
|
|
40695
|
-
mountAs
|
|
40696
|
-
};
|
|
40697
|
-
await tx.run(`UPDATE projects SET repos_json = ? WHERE id = ?`, JSON.stringify([...repos, added]), projectId);
|
|
40698
|
-
return { ok: true, added };
|
|
40699
|
-
});
|
|
40700
|
-
if (!("ok" in outcome)) {
|
|
40701
|
-
return reply.status(outcome.status).send({ error: outcome.error, ...outcome.hint ? { hint: outcome.hint } : {} });
|
|
40702
|
-
}
|
|
40703
|
-
if (!actor.viaCli) {
|
|
40704
|
-
const msg = { type: "provision_repo", projectId, repo: outcome.added };
|
|
40705
|
-
const delivered = sendToProjectCli(projectId, msg);
|
|
40706
|
-
app.log.info({ projectId, repo: repo.name, by: actor.email, delivered }, "repo added, provision_repo dispatched");
|
|
40707
|
-
} else {
|
|
40708
|
-
app.log.info({ projectId, repo: repo.name, by: actor.email }, "repo added by CLI");
|
|
40709
|
-
}
|
|
40710
|
-
return reply.status(201).send(await loadRow(db, projectId));
|
|
40711
|
-
}
|
|
40712
|
-
);
|
|
40713
|
-
app.delete(
|
|
40714
|
-
"/projects/:projectId/repos/:name",
|
|
40715
|
-
async (req, reply) => {
|
|
40716
|
-
const actor = await resolveRepoMutator(db, config, req, reply, req.params.projectId);
|
|
40717
|
-
if (!actor) return reply;
|
|
40718
|
-
const { projectId, name } = req.params;
|
|
40719
|
-
const purge = req.query.purge === "true" || req.query.purge === "1";
|
|
40720
|
-
const outcome = await db.tx(async (tx) => {
|
|
40721
|
-
const row = await tx.get(`SELECT repos_json as reposJson FROM projects WHERE id = ?`, projectId);
|
|
40722
|
-
if (!row) return { status: 404, error: "project_not_found" };
|
|
40723
|
-
const repos = JSON.parse(row.reposJson);
|
|
40724
|
-
const removed = repos.find((r) => r.name === name);
|
|
40725
|
-
if (!removed) return { status: 404, error: "repo_not_found" };
|
|
40726
|
-
await tx.run(
|
|
40727
|
-
`UPDATE projects SET repos_json = ? WHERE id = ?`,
|
|
40728
|
-
JSON.stringify(repos.filter((r) => r.name !== name)),
|
|
40729
|
-
projectId
|
|
40730
|
-
);
|
|
40731
|
-
return { ok: true, removed };
|
|
40732
|
-
});
|
|
40733
|
-
if (!("ok" in outcome)) return reply.status(outcome.status).send({ error: outcome.error });
|
|
40734
|
-
if (outcome.removed.host === "github") {
|
|
40735
|
-
await removeGhRunnerBinding(db, projectId, outcome.removed.owner, outcome.removed.repoName);
|
|
40736
|
-
}
|
|
40737
|
-
if (!actor.viaCli) {
|
|
40738
|
-
const msg = { type: "remove_repo", projectId, repoName: name, ...purge ? { purge: true } : {} };
|
|
40739
|
-
const delivered = sendToProjectCli(projectId, msg);
|
|
40740
|
-
app.log.info({ projectId, repo: name, by: actor.email, purge, delivered }, "repo removed, remove_repo dispatched");
|
|
40741
|
-
} else {
|
|
40742
|
-
app.log.info({ projectId, repo: name, by: actor.email }, "repo removed by CLI");
|
|
40743
|
-
}
|
|
40744
|
-
return reply.status(200).send(await loadRow(db, projectId));
|
|
40745
|
-
}
|
|
40746
|
-
);
|
|
40747
40951
|
app.get(
|
|
40748
40952
|
"/projects/:projectId/tracker-status",
|
|
40749
40953
|
async (req, reply) => {
|
|
@@ -41268,8 +41472,6 @@ async function bugReportRoutes(app, deps) {
|
|
|
41268
41472
|
|
|
41269
41473
|
// src/server.ts
|
|
41270
41474
|
init_ws();
|
|
41271
|
-
init_provision();
|
|
41272
|
-
init_provision2();
|
|
41273
41475
|
init_context_read();
|
|
41274
41476
|
var __dirname = dirname4(fileURLToPath3(import.meta.url));
|
|
41275
41477
|
var DASHBOARD_DIST = resolve3(__dirname, "../../dashboard/dist");
|
|
@@ -41532,13 +41734,28 @@ async function buildServer(config, db) {
|
|
|
41532
41734
|
await apiV1ReadRoutes(instance, {
|
|
41533
41735
|
db,
|
|
41534
41736
|
config,
|
|
41535
|
-
read: createReadOperations(createReadModel(db), authorizer)
|
|
41737
|
+
read: createReadOperations(createReadModel(db, { isOperator: (email) => isSuperUser(config, email) }), authorizer)
|
|
41536
41738
|
});
|
|
41537
41739
|
await apiV1MutationRoutes(instance, {
|
|
41538
41740
|
db,
|
|
41539
41741
|
config,
|
|
41540
41742
|
commands: createCommandOperations({
|
|
41541
|
-
write: createWriteModel(db, app.log
|
|
41743
|
+
write: createWriteModel(db, app.log, {
|
|
41744
|
+
isOperator: (email) => isSuperUser(config, email),
|
|
41745
|
+
// DEV-216: tell the operator a project was registered. Fire-and-
|
|
41746
|
+
// forget — a mail failure must not fail the registration the
|
|
41747
|
+
// client is waiting on.
|
|
41748
|
+
projectRegistered: (n) => {
|
|
41749
|
+
void mailer.sendProjectRegistered(config.registrationNotifyEmail, {
|
|
41750
|
+
projectName: n.name,
|
|
41751
|
+
projectId: n.projectId,
|
|
41752
|
+
automationLevel: n.automationLevel,
|
|
41753
|
+
ownerEmail: n.ownerEmail ?? void 0,
|
|
41754
|
+
repoCount: n.repoCount,
|
|
41755
|
+
projectUrl: `${config.baseUrl}/?project=${encodeURIComponent(n.projectId)}`
|
|
41756
|
+
}).catch((err) => app.log.error({ err, projectId: n.projectId }, "failed to send project-registered notification"));
|
|
41757
|
+
}
|
|
41758
|
+
}),
|
|
41542
41759
|
idempotency: createIdempotencyPort(db),
|
|
41543
41760
|
authorizer,
|
|
41544
41761
|
audit: createCommandAudit(db)
|