@kubitnodes/stellaris 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -2
- package/dist/cli/index.js +167 -12
- package/dist/server/index.js +543 -32
- package/package.json +2 -1
- package/web/dist/assets/ChannelView-O1FQ_Cbf.js +1 -0
- package/web/dist/assets/CitizenView-D0rSRBnj.js +2 -0
- package/web/dist/assets/CitizensView-BPx5ChvL.js +1 -0
- package/web/dist/assets/KnowledgeView-Cjf5Axu5.js +1 -0
- package/web/dist/assets/Markdown-Dkiv3bKi.js +29 -0
- package/web/dist/assets/MessageItem-sep2BGUh.js +1 -0
- package/web/dist/assets/MetricsView-BIH8FLob.js +1 -0
- package/web/dist/assets/NeedsYouView-CuB7daZ0.js +1 -0
- package/web/dist/assets/Overview-C6Ae0n9b.js +1 -0
- package/web/dist/assets/Pane-BXpuXst6.js +1 -0
- package/web/dist/assets/ProjectView-CpTcrvFa.js +1 -0
- package/web/dist/assets/ProposalView-BXisoFis.js +1 -0
- package/web/dist/assets/ProposalsView-BjPeXIxG.js +1 -0
- package/web/dist/assets/RunnersView-0ou0ekMc.js +1 -0
- package/web/dist/assets/SocietyView-C8lU_XY4.js +1 -0
- package/web/dist/assets/SubjectThread-BWbOV_wk.js +1 -0
- package/web/dist/assets/TaskView-CDLRRfS6.js +1 -0
- package/web/dist/assets/TasksView-p6PdvdWY.js +1 -0
- package/web/dist/assets/ThreadCard-BisvLnYg.js +1 -0
- package/web/dist/assets/ThreadForms-DZ1yLsjU.js +1 -0
- package/web/dist/assets/ThreadView-CQUPQ5oR.js +1 -0
- package/web/dist/assets/{citizen-BXVebeiP.js → citizen-BwK8Ciw3.js} +1 -1
- package/web/dist/assets/format-DZLzLk7S.js +55 -0
- package/web/dist/assets/index-CbJnPhSw.js +21 -0
- package/web/dist/assets/index-DJ30ENcc.css +2 -0
- package/web/dist/assets/{tasks-rltmWd5a.js → tasks-DZgkXWwu.js} +1 -1
- package/web/dist/assets/{useStickyScroll-DDS3s21P.js → useStickyScroll-CVTdIoKj.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/ChannelView-B8BZLsf1.js +0 -1
- package/web/dist/assets/CitizenView-GomLDcrr.js +0 -2
- package/web/dist/assets/CitizensView-yNv8WtND.js +0 -1
- package/web/dist/assets/KnowledgeView--jxofSSN.js +0 -1
- package/web/dist/assets/Markdown-BpYzeV5r.js +0 -28
- package/web/dist/assets/MessageItem-BGPpqgpq.js +0 -1
- package/web/dist/assets/MetricsView-CcqLr6S1.js +0 -1
- package/web/dist/assets/NeedsYouView-r7gJGgVG.js +0 -1
- package/web/dist/assets/Overview-Cmz4_gfI.js +0 -1
- package/web/dist/assets/Pane-DRpHmeJ9.js +0 -1
- package/web/dist/assets/ProjectView-CuACZj4L.js +0 -1
- package/web/dist/assets/ProposalView-BiVcNEDL.js +0 -1
- package/web/dist/assets/ProposalsView-BIdkOfZO.js +0 -1
- package/web/dist/assets/SocietyView-Dya97szC.js +0 -1
- package/web/dist/assets/SubjectThread-2j_msY38.js +0 -1
- package/web/dist/assets/TaskView-CcJr7xnH.js +0 -1
- package/web/dist/assets/TasksView-BUvHntvK.js +0 -1
- package/web/dist/assets/ThreadCard-tzMGvn9j.js +0 -1
- package/web/dist/assets/ThreadForms-DE2yJEhz.js +0 -1
- package/web/dist/assets/ThreadView-FsNOTyVC.js +0 -1
- package/web/dist/assets/format-Ca2VAvEx.js +0 -77
- package/web/dist/assets/index-CtuchAFa.css +0 -2
- package/web/dist/assets/index-D4GJkeKU.js +0 -237
package/dist/server/index.js
CHANGED
|
@@ -695,6 +695,9 @@ var BOARD_EVENT_TYPES = [
|
|
|
695
695
|
"knowledge.written",
|
|
696
696
|
"skill.promoted",
|
|
697
697
|
"runner.added",
|
|
698
|
+
"user.signed_in",
|
|
699
|
+
"user.signed_out",
|
|
700
|
+
"user.token_rotated",
|
|
698
701
|
"runner.changed",
|
|
699
702
|
"ops.signal",
|
|
700
703
|
"paused.changed",
|
|
@@ -803,6 +806,36 @@ var RunnerHelloSchema = z6.object({
|
|
|
803
806
|
turns: z6.array(UlidSchema).default([])
|
|
804
807
|
});
|
|
805
808
|
var RunnerWelcomeSchema = z6.object({ name: NameSchema, version: z6.string() });
|
|
809
|
+
var RunnerEnrollRequestSchema = z6.object({
|
|
810
|
+
protocol: z6.number().int().positive(),
|
|
811
|
+
version: z6.string().min(1).max(64),
|
|
812
|
+
hostname: z6.string().min(1).max(253),
|
|
813
|
+
os: RunnerOsSchema,
|
|
814
|
+
clis: z6.array(CliKindSchema).max(8),
|
|
815
|
+
capabilities: z6.array(z6.string().min(1).max(64)).max(32).default([])
|
|
816
|
+
});
|
|
817
|
+
var RunnerEnrollmentSchema = z6.object({
|
|
818
|
+
deviceCode: z6.string().min(1),
|
|
819
|
+
userCode: z6.string().min(1),
|
|
820
|
+
expiresAt: IsoDateTimeSchema,
|
|
821
|
+
intervalMs: z6.number().int().positive()
|
|
822
|
+
});
|
|
823
|
+
var RunnerEnrollPollSchema = z6.object({ deviceCode: z6.string().min(1) });
|
|
824
|
+
var RunnerEnrollStatusSchema = z6.discriminatedUnion("status", [
|
|
825
|
+
z6.object({ status: z6.literal("pending") }),
|
|
826
|
+
z6.object({ status: z6.literal("approved"), name: NameSchema, token: z6.string().min(1) }),
|
|
827
|
+
z6.object({ status: z6.literal("denied") })
|
|
828
|
+
]);
|
|
829
|
+
var PendingEnrollmentSchema = z6.object({
|
|
830
|
+
userCode: z6.string().min(1),
|
|
831
|
+
hostname: z6.string(),
|
|
832
|
+
os: RunnerOsSchema,
|
|
833
|
+
clis: z6.array(CliKindSchema),
|
|
834
|
+
capabilities: z6.array(z6.string()),
|
|
835
|
+
version: z6.string(),
|
|
836
|
+
requestedAt: IsoDateTimeSchema,
|
|
837
|
+
expiresAt: IsoDateTimeSchema
|
|
838
|
+
});
|
|
806
839
|
var ProjectRepoSchema = z6.object({
|
|
807
840
|
slug: NameSchema,
|
|
808
841
|
origin: z6.string().nullable(),
|
|
@@ -1121,6 +1154,12 @@ var MetricsSchema = z9.object({
|
|
|
1121
1154
|
// packages/shared/dist/config.js
|
|
1122
1155
|
import { z as z10 } from "zod";
|
|
1123
1156
|
var LogLevelSchema = z10.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
|
1157
|
+
var GithubSignInSchema = z10.object({
|
|
1158
|
+
clientId: z10.string().min(1),
|
|
1159
|
+
clientSecret: z10.string().min(1),
|
|
1160
|
+
/** Logins, lowercased; GitHub logins are case-insensitive. */
|
|
1161
|
+
users: z10.array(z10.string().min(1)).min(1)
|
|
1162
|
+
});
|
|
1124
1163
|
var ServerConfigSchema = z10.object({
|
|
1125
1164
|
dataDir: z10.string().min(1),
|
|
1126
1165
|
host: z10.string().min(1),
|
|
@@ -1138,7 +1177,14 @@ var ServerConfigSchema = z10.object({
|
|
|
1138
1177
|
/** Rounds of tool calls one Claude turn may take, or null for no limit. */
|
|
1139
1178
|
toolRounds: z10.number().int().positive().nullable(),
|
|
1140
1179
|
/** How long a resident session stays warm after its last turn. */
|
|
1141
|
-
residentIdleMs: z10.number().int().positive()
|
|
1180
|
+
residentIdleMs: z10.number().int().positive(),
|
|
1181
|
+
/** Signing in to the board with GitHub, or null when only a token lets anyone in. */
|
|
1182
|
+
github: GithubSignInSchema.nullable(),
|
|
1183
|
+
/**
|
|
1184
|
+
* Reverse proxies in front of the server whose X-Forwarded-For entries name the client, for the
|
|
1185
|
+
* rate limits on what anyone may ask without a token: 0 when clients connect directly.
|
|
1186
|
+
*/
|
|
1187
|
+
trustedProxies: z10.number().int().min(0).max(16)
|
|
1142
1188
|
});
|
|
1143
1189
|
function limit(value, fallback) {
|
|
1144
1190
|
if (value === void 0 || value.trim() === "") {
|
|
@@ -1146,6 +1192,21 @@ function limit(value, fallback) {
|
|
|
1146
1192
|
}
|
|
1147
1193
|
return value.trim().toLowerCase() === "unlimited" ? null : Number(value);
|
|
1148
1194
|
}
|
|
1195
|
+
function list(value) {
|
|
1196
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1197
|
+
}
|
|
1198
|
+
function githubSignIn(env) {
|
|
1199
|
+
const clientId = env["STELLARIS_GITHUB_CLIENT_ID"] ?? "";
|
|
1200
|
+
const clientSecret = env["STELLARIS_GITHUB_CLIENT_SECRET"] ?? "";
|
|
1201
|
+
const users = list(env["STELLARIS_GITHUB_USERS"]).map((login) => login.toLowerCase());
|
|
1202
|
+
if (clientId === "" && clientSecret === "" && users.length === 0) {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
if (clientId === "" || clientSecret === "" || users.length === 0) {
|
|
1206
|
+
throw new Error("GitHub sign-in needs STELLARIS_GITHUB_CLIENT_ID, STELLARIS_GITHUB_CLIENT_SECRET, and STELLARIS_GITHUB_USERS together");
|
|
1207
|
+
}
|
|
1208
|
+
return GithubSignInSchema.parse({ clientId, clientSecret, users });
|
|
1209
|
+
}
|
|
1149
1210
|
function loadServerConfig(env) {
|
|
1150
1211
|
return ServerConfigSchema.parse({
|
|
1151
1212
|
dataDir: env["STELLARIS_DATA_DIR"] ?? "./data",
|
|
@@ -1156,14 +1217,19 @@ function loadServerConfig(env) {
|
|
|
1156
1217
|
concurrency: limit(env["STELLARIS_CONCURRENCY"], null),
|
|
1157
1218
|
turnTimeoutMs: limit(env["STELLARIS_TURN_TIMEOUT_MS"], 20 * 6e4),
|
|
1158
1219
|
toolRounds: limit(env["STELLARIS_TOOL_ROUNDS"], 60),
|
|
1159
|
-
residentIdleMs: Number(env["STELLARIS_RESIDENT_IDLE_MS"] ?? String(10 * 6e4))
|
|
1220
|
+
residentIdleMs: Number(env["STELLARIS_RESIDENT_IDLE_MS"] ?? String(10 * 6e4)),
|
|
1221
|
+
github: githubSignIn(env),
|
|
1222
|
+
trustedProxies: Number(env["STELLARIS_TRUSTED_PROXIES"] ?? "0")
|
|
1160
1223
|
});
|
|
1161
1224
|
}
|
|
1162
1225
|
var RunnerConfigSchema = z10.object({
|
|
1163
1226
|
/** The board server, for example http://127.0.0.1:4700. */
|
|
1164
1227
|
serverUrl: z10.string().url(),
|
|
1165
|
-
/**
|
|
1166
|
-
|
|
1228
|
+
/**
|
|
1229
|
+
* The runner's token, from `runner add`, or null for the one it saved when it was enrolled, or
|
|
1230
|
+
* to enroll it.
|
|
1231
|
+
*/
|
|
1232
|
+
token: z10.string().min(1).nullable(),
|
|
1167
1233
|
/** The runner's own data directory: the board mirror, agent homes, repositories, worktrees. */
|
|
1168
1234
|
dataDir: z10.string().min(1),
|
|
1169
1235
|
logLevel: LogLevelSchema,
|
|
@@ -1174,7 +1240,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1174
1240
|
});
|
|
1175
1241
|
|
|
1176
1242
|
// packages/shared/dist/version.js
|
|
1177
|
-
var STELLARIS_VERSION = "0.
|
|
1243
|
+
var STELLARIS_VERSION = "0.2.0";
|
|
1178
1244
|
|
|
1179
1245
|
// packages/board-core/dist/errors.js
|
|
1180
1246
|
var BoardError = class extends Error {
|
|
@@ -1228,9 +1294,9 @@ async function readMarkdown(file, schema) {
|
|
|
1228
1294
|
return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
|
|
1229
1295
|
}
|
|
1230
1296
|
async function writeMarkdown(file, data, body) {
|
|
1231
|
-
const
|
|
1297
|
+
const normalized2 = body.endsWith("\n") || body.length === 0 ? body : `${body}
|
|
1232
1298
|
`;
|
|
1233
|
-
await writeFileAtomic(file, matter.stringify(
|
|
1299
|
+
await writeFileAtomic(file, matter.stringify(normalized2, data));
|
|
1234
1300
|
}
|
|
1235
1301
|
function isMissing(error) {
|
|
1236
1302
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
@@ -1969,11 +2035,11 @@ function computeMetrics(input) {
|
|
|
1969
2035
|
unanswered: waiting.length,
|
|
1970
2036
|
medianMs: median(allLatencies),
|
|
1971
2037
|
slowestMs: allLatencies.length === 0 ? null : Math.max(...allLatencies),
|
|
1972
|
-
byAgent: [...latencies].map(([agent,
|
|
2038
|
+
byAgent: [...latencies].map(([agent, list2]) => ({
|
|
1973
2039
|
agent,
|
|
1974
|
-
mentions:
|
|
1975
|
-
medianMs: median(
|
|
1976
|
-
slowestMs: Math.max(...
|
|
2040
|
+
mentions: list2.length,
|
|
2041
|
+
medianMs: median(list2) ?? 0,
|
|
2042
|
+
slowestMs: Math.max(...list2)
|
|
1977
2043
|
})).toSorted((a, b) => b.medianMs - a.medianMs)
|
|
1978
2044
|
},
|
|
1979
2045
|
decisions: {
|
|
@@ -2207,6 +2273,14 @@ var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".cod
|
|
|
2207
2273
|
var RunnerTokensSchema = z12.object({
|
|
2208
2274
|
runners: z12.record(z12.string(), z12.object({ tokenHash: z12.string().min(1) }))
|
|
2209
2275
|
});
|
|
2276
|
+
var SignInsSchema = z12.object({
|
|
2277
|
+
signIns: z12.array(z12.object({
|
|
2278
|
+
tokenHash: z12.string().min(1),
|
|
2279
|
+
login: z12.string().min(1),
|
|
2280
|
+
createdAt: IsoDateTimeSchema,
|
|
2281
|
+
expiresAt: IsoDateTimeSchema
|
|
2282
|
+
})).default([])
|
|
2283
|
+
});
|
|
2210
2284
|
var TurnHistoryPayloadSchema = z12.object({
|
|
2211
2285
|
project: NameSchema,
|
|
2212
2286
|
thread: UlidSchema.optional(),
|
|
@@ -2412,6 +2486,8 @@ var Board = class _Board {
|
|
|
2412
2486
|
turnTokens = /* @__PURE__ */ new Map();
|
|
2413
2487
|
/** Runner token hashes to runner names. */
|
|
2414
2488
|
runnerTokens = /* @__PURE__ */ new Map();
|
|
2489
|
+
/** Sign-ins by token hash, each acting as the user until it expires. */
|
|
2490
|
+
signIns = /* @__PURE__ */ new Map();
|
|
2415
2491
|
files = new FileTree();
|
|
2416
2492
|
homes;
|
|
2417
2493
|
constructor(dataDir, options) {
|
|
@@ -2440,6 +2516,7 @@ var Board = class _Board {
|
|
|
2440
2516
|
await board2.mutex.run(() => board2.applyAlignments());
|
|
2441
2517
|
await board2.loadTokenIndex();
|
|
2442
2518
|
await board2.loadRunnerTokens();
|
|
2519
|
+
await board2.loadSignIns();
|
|
2443
2520
|
await board2.homes.installHooks();
|
|
2444
2521
|
return board2;
|
|
2445
2522
|
}
|
|
@@ -2762,6 +2839,14 @@ ${charter.purpose}
|
|
|
2762
2839
|
if (persistent !== void 0) {
|
|
2763
2840
|
return persistent;
|
|
2764
2841
|
}
|
|
2842
|
+
const signIn = this.signIns.get(hash);
|
|
2843
|
+
if (signIn !== void 0) {
|
|
2844
|
+
if (signIn.expiresAt > this.now().getTime()) {
|
|
2845
|
+
return this.userActor();
|
|
2846
|
+
}
|
|
2847
|
+
this.signIns.delete(hash);
|
|
2848
|
+
return null;
|
|
2849
|
+
}
|
|
2765
2850
|
const temporary = this.turnTokens.get(hash);
|
|
2766
2851
|
if (temporary === void 0) {
|
|
2767
2852
|
return null;
|
|
@@ -2806,6 +2891,77 @@ ${charter.purpose}
|
|
|
2806
2891
|
userActor() {
|
|
2807
2892
|
return { name: USER_NAME, role: USER_ROLE };
|
|
2808
2893
|
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Signs the user in, as `login` on the identity provider, for `ttlMs`, and returns the sign-in's
|
|
2896
|
+
* token, shown once. Only its hash is kept, in `state/sign-ins.json`, so a sign-in outlives a
|
|
2897
|
+
* restart; it acts as the user, like the user's own token, until it expires or is signed out.
|
|
2898
|
+
*/
|
|
2899
|
+
async signIn(login, ttlMs) {
|
|
2900
|
+
return this.mutex.run(async () => {
|
|
2901
|
+
const token = mintToken();
|
|
2902
|
+
const now = this.now().getTime();
|
|
2903
|
+
const record = {
|
|
2904
|
+
tokenHash: hashToken(token),
|
|
2905
|
+
login,
|
|
2906
|
+
createdAt: new Date(now).toISOString(),
|
|
2907
|
+
expiresAt: new Date(now + ttlMs).toISOString()
|
|
2908
|
+
};
|
|
2909
|
+
const live = (await this.readSignIns()).filter((each) => Date.parse(each.expiresAt) > now);
|
|
2910
|
+
await writeJson(this.signInsFile(), { signIns: [...live, record] });
|
|
2911
|
+
this.signIns.set(record.tokenHash, { login, expiresAt: Date.parse(record.expiresAt) });
|
|
2912
|
+
await this.events.append("user.signed_in", USER_NAME, { login });
|
|
2913
|
+
return token;
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
/** Ends the sign-in a token belongs to; false for a token that is no sign-in's. */
|
|
2917
|
+
async signOut(token) {
|
|
2918
|
+
const hash = hashToken(token);
|
|
2919
|
+
return this.mutex.run(async () => {
|
|
2920
|
+
const signIn = this.signIns.get(hash);
|
|
2921
|
+
if (signIn === void 0) {
|
|
2922
|
+
return false;
|
|
2923
|
+
}
|
|
2924
|
+
this.signIns.delete(hash);
|
|
2925
|
+
await writeJson(this.signInsFile(), {
|
|
2926
|
+
signIns: (await this.readSignIns()).filter((each) => each.tokenHash !== hash)
|
|
2927
|
+
});
|
|
2928
|
+
await this.events.append("user.signed_out", USER_NAME, { login: signIn.login });
|
|
2929
|
+
return true;
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Issues the user a new token and revokes the old one, returning the new one, shown once: the way
|
|
2934
|
+
* back in when the token is lost. A server already running keeps the old token's hash in memory
|
|
2935
|
+
* until it restarts, since the admin CLI that calls this writes the data directory beside it.
|
|
2936
|
+
*/
|
|
2937
|
+
async rotateUserToken() {
|
|
2938
|
+
return this.mutex.run(async () => {
|
|
2939
|
+
const previous = await this.readAgent(USER_NAME);
|
|
2940
|
+
const token = mintToken();
|
|
2941
|
+
await this.updateAgent(USER_NAME, (agent) => ({ ...agent, tokenHash: hashToken(token) }));
|
|
2942
|
+
this.tokenIndex.delete(previous.tokenHash);
|
|
2943
|
+
this.tokenIndex.set(hashToken(token), this.userActor());
|
|
2944
|
+
await this.events.append("user.token_rotated", USER_NAME, {});
|
|
2945
|
+
return token;
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
signInsFile() {
|
|
2949
|
+
return path6.join(this.paths.state(), "sign-ins.json");
|
|
2950
|
+
}
|
|
2951
|
+
async readSignIns() {
|
|
2952
|
+
const file = this.signInsFile();
|
|
2953
|
+
return await exists(file) ? (await readJson(file, SignInsSchema)).signIns : [];
|
|
2954
|
+
}
|
|
2955
|
+
async loadSignIns() {
|
|
2956
|
+
this.signIns.clear();
|
|
2957
|
+
const now = this.now().getTime();
|
|
2958
|
+
for (const signIn of await this.readSignIns()) {
|
|
2959
|
+
const expiresAt = Date.parse(signIn.expiresAt);
|
|
2960
|
+
if (expiresAt > now) {
|
|
2961
|
+
this.signIns.set(signIn.tokenHash, { login: signIn.login, expiresAt });
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2809
2965
|
async actorFor(name) {
|
|
2810
2966
|
const agent = await this.readAgent(name);
|
|
2811
2967
|
return { name: agent.name, role: agent.role };
|
|
@@ -4802,8 +4958,9 @@ ${rationale}`}`);
|
|
|
4802
4958
|
/**
|
|
4803
4959
|
* Registers a runner and mints its token, returned once; only its hash is kept, outside the
|
|
4804
4960
|
* projection. The runner reports its operating system, CLIs, and capabilities when it connects.
|
|
4961
|
+
* An enrolled runner names the machine that asked, which the event records.
|
|
4805
4962
|
*/
|
|
4806
|
-
async addRunner(actor, name) {
|
|
4963
|
+
async addRunner(actor, name, enrolled) {
|
|
4807
4964
|
if (actor.role !== USER_ROLE) {
|
|
4808
4965
|
throw new BoardError("FORBIDDEN", "only the user adds runners");
|
|
4809
4966
|
}
|
|
@@ -4827,7 +4984,10 @@ ${rationale}`}`);
|
|
|
4827
4984
|
runners: { ...tokens, [parsed]: { tokenHash: hashToken(token) } }
|
|
4828
4985
|
});
|
|
4829
4986
|
this.runnerTokens.set(hashToken(token), parsed);
|
|
4830
|
-
await this.events.append("runner.added", actor.name, {
|
|
4987
|
+
await this.events.append("runner.added", actor.name, {
|
|
4988
|
+
name: parsed,
|
|
4989
|
+
...enrolled === void 0 ? {} : { enrolledFrom: enrolled.hostname }
|
|
4990
|
+
});
|
|
4831
4991
|
return { runner, token };
|
|
4832
4992
|
});
|
|
4833
4993
|
}
|
|
@@ -8282,14 +8442,205 @@ async function handleMcpRequest(board2, actor, request, version) {
|
|
|
8282
8442
|
}
|
|
8283
8443
|
|
|
8284
8444
|
// apps/server/dist/app.js
|
|
8285
|
-
import { Hono as
|
|
8445
|
+
import { Hono as Hono3 } from "hono";
|
|
8286
8446
|
import { compress } from "hono/compress";
|
|
8287
8447
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
8288
|
-
import { z as
|
|
8448
|
+
import { z as z15, ZodError } from "zod";
|
|
8449
|
+
|
|
8450
|
+
// apps/server/dist/github-auth.js
|
|
8451
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
8452
|
+
import { Hono } from "hono";
|
|
8453
|
+
import { z as z14 } from "zod";
|
|
8454
|
+
var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
|
|
8455
|
+
var STATE_TTL_MS = 10 * 6e4;
|
|
8456
|
+
var MAX_STATES = 1e3;
|
|
8457
|
+
var TokenAnswerSchema = z14.object({ access_token: z14.string().min(1) });
|
|
8458
|
+
var GithubUserSchema = z14.object({ login: z14.string().min(1), id: z14.number().int() });
|
|
8459
|
+
function safeNext(next) {
|
|
8460
|
+
return next !== void 0 && /^\/(?![/\\])/.test(next) ? next : "/";
|
|
8461
|
+
}
|
|
8462
|
+
function land(c, next, fragment) {
|
|
8463
|
+
return c.redirect(`${next}#${fragment}`, 302);
|
|
8464
|
+
}
|
|
8465
|
+
function githubAuthRoutes(options) {
|
|
8466
|
+
const { board: board2, github } = options;
|
|
8467
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
8468
|
+
const now = options.now ?? Date.now;
|
|
8469
|
+
const states = /* @__PURE__ */ new Map();
|
|
8470
|
+
const routes = new Hono();
|
|
8471
|
+
routes.get("/config", (c) => c.json({ github: github !== null }));
|
|
8472
|
+
if (github === null) {
|
|
8473
|
+
return routes;
|
|
8474
|
+
}
|
|
8475
|
+
routes.get("/github", options.limit ?? ((_c, next) => next()), (c) => {
|
|
8476
|
+
for (const [state2, pending] of states) {
|
|
8477
|
+
if (pending.expiresAt <= now() || states.size >= MAX_STATES) {
|
|
8478
|
+
states.delete(state2);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
const state = randomBytes2(24).toString("base64url");
|
|
8482
|
+
states.set(state, { next: safeNext(c.req.query("next")), expiresAt: now() + STATE_TTL_MS });
|
|
8483
|
+
const authorize = new URL("https://github.com/login/oauth/authorize");
|
|
8484
|
+
authorize.searchParams.set("client_id", github.clientId);
|
|
8485
|
+
authorize.searchParams.set("state", state);
|
|
8486
|
+
authorize.searchParams.set("allow_signup", "false");
|
|
8487
|
+
return c.redirect(authorize.toString(), 302);
|
|
8488
|
+
});
|
|
8489
|
+
routes.get("/github/callback", async (c) => {
|
|
8490
|
+
const state = c.req.query("state") ?? "";
|
|
8491
|
+
const pending = states.get(state);
|
|
8492
|
+
states.delete(state);
|
|
8493
|
+
if (pending === void 0 || pending.expiresAt <= now()) {
|
|
8494
|
+
return land(c, "/", "signin-error=expired");
|
|
8495
|
+
}
|
|
8496
|
+
const code = c.req.query("code");
|
|
8497
|
+
if (code === void 0 || code === "") {
|
|
8498
|
+
return land(c, pending.next, "signin-error=declined");
|
|
8499
|
+
}
|
|
8500
|
+
let login;
|
|
8501
|
+
let id;
|
|
8502
|
+
try {
|
|
8503
|
+
const tokenAnswer = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
8504
|
+
method: "POST",
|
|
8505
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
8506
|
+
body: JSON.stringify({
|
|
8507
|
+
client_id: github.clientId,
|
|
8508
|
+
client_secret: github.clientSecret,
|
|
8509
|
+
code
|
|
8510
|
+
})
|
|
8511
|
+
});
|
|
8512
|
+
const { access_token: accessToken } = TokenAnswerSchema.parse(await tokenAnswer.json());
|
|
8513
|
+
const userAnswer = await fetchImpl("https://api.github.com/user", {
|
|
8514
|
+
headers: {
|
|
8515
|
+
accept: "application/vnd.github+json",
|
|
8516
|
+
authorization: `Bearer ${accessToken}`,
|
|
8517
|
+
"user-agent": "stellaris"
|
|
8518
|
+
}
|
|
8519
|
+
});
|
|
8520
|
+
if (!userAnswer.ok) {
|
|
8521
|
+
throw new Error(`GitHub answered ${userAnswer.status} for the user`);
|
|
8522
|
+
}
|
|
8523
|
+
({ login, id } = GithubUserSchema.parse(await userAnswer.json()));
|
|
8524
|
+
} catch (error) {
|
|
8525
|
+
options.log?.warn({ error: String(error) }, "GitHub sign-in failed");
|
|
8526
|
+
return land(c, pending.next, "signin-error=github");
|
|
8527
|
+
}
|
|
8528
|
+
if (!github.users.includes(login.toLowerCase()) && !github.users.includes(String(id))) {
|
|
8529
|
+
options.log?.warn({ login }, "GitHub sign-in refused a login that is not on the list");
|
|
8530
|
+
return land(c, pending.next, "signin-error=not-allowed");
|
|
8531
|
+
}
|
|
8532
|
+
const token = await board2.signIn(login, SIGN_IN_TTL_MS);
|
|
8533
|
+
return land(c, pending.next, `session=${encodeURIComponent(token)}`);
|
|
8534
|
+
});
|
|
8535
|
+
return routes;
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8538
|
+
// apps/server/dist/rate-limit.js
|
|
8539
|
+
import { isIPv4, isIPv6 } from "node:net";
|
|
8540
|
+
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
8541
|
+
var ENROLL_LIMIT = { limit: 5, windowMs: 10 * 6e4 };
|
|
8542
|
+
var SIGN_IN_LIMIT = { limit: 20, windowMs: 10 * 6e4 };
|
|
8543
|
+
var MAX_ADDRESSES = 1e4;
|
|
8544
|
+
var RateLimiter = class {
|
|
8545
|
+
options;
|
|
8546
|
+
hits = /* @__PURE__ */ new Map();
|
|
8547
|
+
constructor(options) {
|
|
8548
|
+
this.options = options;
|
|
8549
|
+
}
|
|
8550
|
+
take(key) {
|
|
8551
|
+
const now = (this.options.now ?? Date.now)();
|
|
8552
|
+
const since = now - this.options.windowMs;
|
|
8553
|
+
let entry = this.hits.get(key);
|
|
8554
|
+
if (entry === void 0) {
|
|
8555
|
+
this.makeRoom(since);
|
|
8556
|
+
entry = { times: [], warned: false };
|
|
8557
|
+
}
|
|
8558
|
+
this.hits.delete(key);
|
|
8559
|
+
this.hits.set(key, entry);
|
|
8560
|
+
entry.times = entry.times.filter((time) => time > since);
|
|
8561
|
+
const oldest = entry.times[0];
|
|
8562
|
+
if (oldest !== void 0 && entry.times.length >= this.options.limit) {
|
|
8563
|
+
const first = !entry.warned;
|
|
8564
|
+
entry.warned = true;
|
|
8565
|
+
return { ok: false, retryAfterMs: oldest + this.options.windowMs - now, first };
|
|
8566
|
+
}
|
|
8567
|
+
entry.times.push(now);
|
|
8568
|
+
entry.warned = false;
|
|
8569
|
+
return { ok: true };
|
|
8570
|
+
}
|
|
8571
|
+
makeRoom(since) {
|
|
8572
|
+
if (this.hits.size < MAX_ADDRESSES) {
|
|
8573
|
+
return;
|
|
8574
|
+
}
|
|
8575
|
+
for (const [key, entry] of this.hits) {
|
|
8576
|
+
if (entry.times.every((time) => time <= since)) {
|
|
8577
|
+
this.hits.delete(key);
|
|
8578
|
+
}
|
|
8579
|
+
}
|
|
8580
|
+
for (const key of this.hits.keys()) {
|
|
8581
|
+
if (this.hits.size < MAX_ADDRESSES) {
|
|
8582
|
+
break;
|
|
8583
|
+
}
|
|
8584
|
+
this.hits.delete(key);
|
|
8585
|
+
}
|
|
8586
|
+
}
|
|
8587
|
+
};
|
|
8588
|
+
function clientAddress(c, trustedProxies) {
|
|
8589
|
+
let socket = "unknown";
|
|
8590
|
+
try {
|
|
8591
|
+
socket = getConnInfo(c).remote.address ?? socket;
|
|
8592
|
+
} catch {
|
|
8593
|
+
}
|
|
8594
|
+
const forwarded = (c.req.header("x-forwarded-for") ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
8595
|
+
const chain = [socket, ...forwarded.toReversed()];
|
|
8596
|
+
return chain[Math.min(trustedProxies, chain.length - 1)] ?? socket;
|
|
8597
|
+
}
|
|
8598
|
+
function groups(part) {
|
|
8599
|
+
return part === "" ? [] : part.split(":");
|
|
8600
|
+
}
|
|
8601
|
+
function width(parts) {
|
|
8602
|
+
return parts.reduce((sum, part) => sum + (part.includes(".") ? 2 : 1), 0);
|
|
8603
|
+
}
|
|
8604
|
+
function addressKey(address) {
|
|
8605
|
+
const unwrapped = /^\[(.+)\](?::\d+)?$/.exec(address)?.[1] ?? /^(\d+\.\d+\.\d+\.\d+):\d+$/.exec(address)?.[1] ?? address;
|
|
8606
|
+
const bare = unwrapped.split("%")[0] ?? unwrapped;
|
|
8607
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(bare)?.[1];
|
|
8608
|
+
if (mapped !== void 0) {
|
|
8609
|
+
return mapped;
|
|
8610
|
+
}
|
|
8611
|
+
if (isIPv4(bare) || !isIPv6(bare)) {
|
|
8612
|
+
return bare.slice(0, 64);
|
|
8613
|
+
}
|
|
8614
|
+
const [head = "", tail] = bare.split("::");
|
|
8615
|
+
const front = groups(head);
|
|
8616
|
+
const back = tail === void 0 ? [] : groups(tail);
|
|
8617
|
+
const filled = tail === void 0 ? front : [...front, ...Array.from({ length: 8 - width(front) - width(back) }, () => "0"), ...back];
|
|
8618
|
+
const prefix = filled.slice(0, 4).map((group) => Number.parseInt(group, 16).toString(16));
|
|
8619
|
+
return `${prefix.join(":")}::/64`;
|
|
8620
|
+
}
|
|
8621
|
+
function rateLimit(options) {
|
|
8622
|
+
return async (c, next) => {
|
|
8623
|
+
const address = clientAddress(c, options.trustedProxies);
|
|
8624
|
+
const verdict = options.limiter.take(addressKey(address));
|
|
8625
|
+
if (!verdict.ok) {
|
|
8626
|
+
const seconds = Math.max(1, Math.ceil(verdict.retryAfterMs / 1e3));
|
|
8627
|
+
if (verdict.first) {
|
|
8628
|
+
options.log?.warn({ address, retryAfterSeconds: seconds }, `rate limited ${options.what}`);
|
|
8629
|
+
}
|
|
8630
|
+
c.header("retry-after", String(seconds));
|
|
8631
|
+
return c.json({
|
|
8632
|
+
error: "RATE_LIMITED",
|
|
8633
|
+
message: `too many ${options.what} from this address; try again in ${seconds} seconds`
|
|
8634
|
+
}, 429);
|
|
8635
|
+
}
|
|
8636
|
+
await next();
|
|
8637
|
+
return void 0;
|
|
8638
|
+
};
|
|
8639
|
+
}
|
|
8289
8640
|
|
|
8290
8641
|
// apps/server/dist/runner-routes.js
|
|
8291
8642
|
import path7 from "node:path";
|
|
8292
|
-
import { Hono } from "hono";
|
|
8643
|
+
import { Hono as Hono2 } from "hono";
|
|
8293
8644
|
import { streamSSE } from "hono/streaming";
|
|
8294
8645
|
|
|
8295
8646
|
// apps/server/dist/home-git.js
|
|
@@ -8385,8 +8736,16 @@ function bearer(c) {
|
|
|
8385
8736
|
const match = /^Bearer\s+(.+)$/i.exec(c.req.header("authorization") ?? "");
|
|
8386
8737
|
return match?.[1]?.trim() ?? null;
|
|
8387
8738
|
}
|
|
8388
|
-
function runnerRoutes(board2, hub) {
|
|
8389
|
-
const routes = new
|
|
8739
|
+
function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
8740
|
+
const routes = new Hono2();
|
|
8741
|
+
if (enrollments !== void 0) {
|
|
8742
|
+
routes.post("/enroll", enrollLimit ?? ((_c, next) => next()), async (c) => c.json(enrollments.open(RunnerEnrollRequestSchema.parse(await c.req.json()))));
|
|
8743
|
+
routes.post("/enroll/poll", async (c) => {
|
|
8744
|
+
const { deviceCode } = RunnerEnrollPollSchema.parse(await c.req.json());
|
|
8745
|
+
const status = enrollments.poll(deviceCode);
|
|
8746
|
+
return status === null ? c.json({ error: "NOT_FOUND", message: "no such enrollment; it may have expired" }, 404) : c.json(status);
|
|
8747
|
+
});
|
|
8748
|
+
}
|
|
8390
8749
|
routes.use("*", async (c, next) => {
|
|
8391
8750
|
const token = bearer(c);
|
|
8392
8751
|
const runner = token === null ? null : board2.resolveRunnerToken(token);
|
|
@@ -8450,7 +8809,7 @@ function runnerRoutes(board2, hub) {
|
|
|
8450
8809
|
});
|
|
8451
8810
|
routes.get("/board/manifest", async (c) => c.json({ files: await board2.boardManifest() }));
|
|
8452
8811
|
routes.post("/board/read", async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
|
|
8453
|
-
const home = new
|
|
8812
|
+
const home = new Hono2();
|
|
8454
8813
|
home.use("*", async (c, next) => {
|
|
8455
8814
|
const agent = NameSchema.parse(c.req.param("agent"));
|
|
8456
8815
|
if (!hub.mayTouchHome(c.get("runner"), agent)) {
|
|
@@ -8550,15 +8909,16 @@ var ERROR_STATUS = {
|
|
|
8550
8909
|
INVALID_TRANSITION: 409,
|
|
8551
8910
|
INVALID_STATE: 409
|
|
8552
8911
|
};
|
|
8553
|
-
var RetireBodySchema =
|
|
8554
|
-
var RunnerBodySchema =
|
|
8555
|
-
var
|
|
8556
|
-
var
|
|
8557
|
-
var
|
|
8558
|
-
var
|
|
8912
|
+
var RetireBodySchema = z15.object({ reason: z15.string().min(1) });
|
|
8913
|
+
var RunnerBodySchema = z15.object({ name: NameSchema });
|
|
8914
|
+
var EnrollmentApprovalSchema = z15.object({ name: NameSchema });
|
|
8915
|
+
var HomeRunnerBodySchema = z15.object({ runner: NameSchema.nullable() });
|
|
8916
|
+
var ModelBodySchema = z15.object({ model: ModelNameSchema.nullable() });
|
|
8917
|
+
var HistoryLimitSchema = z15.coerce.number().int().min(1).max(500);
|
|
8918
|
+
var ChannelBodySchema = z15.object({
|
|
8559
8919
|
project: NameSchema.nullable().default(null),
|
|
8560
8920
|
name: NameSchema,
|
|
8561
|
-
purpose:
|
|
8921
|
+
purpose: z15.string().min(1)
|
|
8562
8922
|
});
|
|
8563
8923
|
function bearer2(c) {
|
|
8564
8924
|
const header = c.req.header("authorization") ?? "";
|
|
@@ -8567,7 +8927,7 @@ function bearer2(c) {
|
|
|
8567
8927
|
}
|
|
8568
8928
|
function createApp(deps) {
|
|
8569
8929
|
const { board: board2, version, turns: turns2, scheduler: scheduler2, models } = deps;
|
|
8570
|
-
const app2 = new
|
|
8930
|
+
const app2 = new Hono3();
|
|
8571
8931
|
app2.get("/health", (c) => c.json({ ok: true, version }));
|
|
8572
8932
|
app2.onError((error, c) => {
|
|
8573
8933
|
if (isBoardError(error)) {
|
|
@@ -8590,9 +8950,10 @@ function createApp(deps) {
|
|
|
8590
8950
|
c.set("actor", actor);
|
|
8591
8951
|
await next();
|
|
8592
8952
|
};
|
|
8593
|
-
const api = new
|
|
8953
|
+
const api = new Hono3();
|
|
8594
8954
|
api.use("*", authenticate);
|
|
8595
8955
|
api.get("/me", (c) => c.json(c.get("actor")));
|
|
8956
|
+
api.delete("/sign-in", async (c) => c.json({ signedOut: await board2.signOut(bearer2(c) ?? "") }));
|
|
8596
8957
|
api.get("/society", async (c) => c.json(await board2.society()));
|
|
8597
8958
|
api.get("/agents", async (c) => c.json((await board2.listAgents()).map(({ tokenHash: _hash, ...agent }) => agent)));
|
|
8598
8959
|
api.get("/members", async (c) => c.json(await board2.listMembers()));
|
|
@@ -8621,6 +8982,28 @@ function createApp(deps) {
|
|
|
8621
8982
|
const body = RunnerBodySchema.parse(await c.req.json());
|
|
8622
8983
|
return c.json(await board2.addRunner(c.get("actor"), body.name));
|
|
8623
8984
|
});
|
|
8985
|
+
const enrollments = deps.enrollments;
|
|
8986
|
+
if (enrollments !== void 0) {
|
|
8987
|
+
const forUser = (c) => {
|
|
8988
|
+
if (c.get("actor").role !== USER_ROLE) {
|
|
8989
|
+
throw new BoardError("FORBIDDEN", "only the user enrolls runners");
|
|
8990
|
+
}
|
|
8991
|
+
};
|
|
8992
|
+
api.get("/enrollments", (c) => {
|
|
8993
|
+
forUser(c);
|
|
8994
|
+
return c.json(enrollments.waiting());
|
|
8995
|
+
});
|
|
8996
|
+
api.post("/enrollments/:code/approve", async (c) => {
|
|
8997
|
+
forUser(c);
|
|
8998
|
+
const body = EnrollmentApprovalSchema.parse(await c.req.json());
|
|
8999
|
+
return c.json(await enrollments.approve(c.get("actor"), c.req.param("code"), body.name));
|
|
9000
|
+
});
|
|
9001
|
+
api.post("/enrollments/:code/deny", (c) => {
|
|
9002
|
+
forUser(c);
|
|
9003
|
+
enrollments.deny(c.req.param("code"));
|
|
9004
|
+
return c.json({ denied: true });
|
|
9005
|
+
});
|
|
9006
|
+
}
|
|
8624
9007
|
api.get("/proposals", async (c) => c.json(await board2.listProposals()));
|
|
8625
9008
|
api.get("/proposals/:id", async (c) => c.json(await board2.readProposal(c.req.param("id"))));
|
|
8626
9009
|
api.get("/signals", async (c) => c.json(await board2.listSignals(Number(c.req.query("limit") ?? "100"))));
|
|
@@ -8654,7 +9037,7 @@ function createApp(deps) {
|
|
|
8654
9037
|
});
|
|
8655
9038
|
api.put("/roles/:name", async (c) => {
|
|
8656
9039
|
const charter = RoleCharterSchema.parse({
|
|
8657
|
-
...
|
|
9040
|
+
...z15.record(z15.string(), z15.unknown()).parse(await c.req.json()),
|
|
8658
9041
|
name: c.req.param("name")
|
|
8659
9042
|
});
|
|
8660
9043
|
return c.json(await board2.setRoleCharter(c.get("actor"), charter));
|
|
@@ -8769,8 +9152,16 @@ function createApp(deps) {
|
|
|
8769
9152
|
return c.json(result ?? null);
|
|
8770
9153
|
});
|
|
8771
9154
|
app2.route("/api", api);
|
|
9155
|
+
const trustedProxies = deps.trustedProxies ?? 0;
|
|
9156
|
+
const limited = (limit2, what) => rateLimit({ limiter: new RateLimiter(limit2), trustedProxies, what, log: deps.log });
|
|
9157
|
+
app2.route("/auth", githubAuthRoutes({
|
|
9158
|
+
board: board2,
|
|
9159
|
+
github: null,
|
|
9160
|
+
...deps.signIn,
|
|
9161
|
+
limit: limited(SIGN_IN_LIMIT, "sign-ins")
|
|
9162
|
+
}));
|
|
8772
9163
|
if (deps.runners !== void 0) {
|
|
8773
|
-
app2.route("/runner", runnerRoutes(board2, deps.runners));
|
|
9164
|
+
app2.route("/runner", runnerRoutes(board2, deps.runners, deps.enrollments, limited(ENROLL_LIMIT, "enrollment requests")));
|
|
8774
9165
|
}
|
|
8775
9166
|
app2.all("/mcp", async (c) => {
|
|
8776
9167
|
const token = bearer2(c);
|
|
@@ -8786,6 +9177,113 @@ function createApp(deps) {
|
|
|
8786
9177
|
return app2;
|
|
8787
9178
|
}
|
|
8788
9179
|
|
|
9180
|
+
// apps/server/dist/enrollment.js
|
|
9181
|
+
import { randomBytes as randomBytes3, randomInt } from "node:crypto";
|
|
9182
|
+
var ENROLLMENT_TTL_MS = 10 * 6e4;
|
|
9183
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
9184
|
+
var MAX_WAITING = 100;
|
|
9185
|
+
var CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ23456789";
|
|
9186
|
+
function codeHalf() {
|
|
9187
|
+
return Array.from({ length: 4 }, () => CODE_ALPHABET.charAt(randomInt(CODE_ALPHABET.length))).join("");
|
|
9188
|
+
}
|
|
9189
|
+
function userCode() {
|
|
9190
|
+
return `${codeHalf()}-${codeHalf()}`;
|
|
9191
|
+
}
|
|
9192
|
+
function normalized(code) {
|
|
9193
|
+
return code.toUpperCase().replaceAll(/[^A-Z0-9]/g, "");
|
|
9194
|
+
}
|
|
9195
|
+
var EnrollmentDesk = class {
|
|
9196
|
+
board;
|
|
9197
|
+
enrollments = /* @__PURE__ */ new Map();
|
|
9198
|
+
now;
|
|
9199
|
+
pollIntervalMs;
|
|
9200
|
+
constructor(board2, options = {}) {
|
|
9201
|
+
this.board = board2;
|
|
9202
|
+
this.now = options.now ?? Date.now;
|
|
9203
|
+
this.pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
9204
|
+
}
|
|
9205
|
+
open(request) {
|
|
9206
|
+
this.prune();
|
|
9207
|
+
const waiting = [...this.enrollments.values()].filter((each) => each.status.status === "pending");
|
|
9208
|
+
if (waiting.length >= MAX_WAITING) {
|
|
9209
|
+
throw new BoardError("INVALID_STATE", `${MAX_WAITING} runners already wait for approval; try again once some have expired`);
|
|
9210
|
+
}
|
|
9211
|
+
const enrollment = {
|
|
9212
|
+
deviceCode: randomBytes3(32).toString("base64url"),
|
|
9213
|
+
userCode: userCode(),
|
|
9214
|
+
request,
|
|
9215
|
+
requestedAt: this.now(),
|
|
9216
|
+
expiresAt: this.now() + ENROLLMENT_TTL_MS,
|
|
9217
|
+
status: { status: "pending" }
|
|
9218
|
+
};
|
|
9219
|
+
this.enrollments.set(enrollment.deviceCode, enrollment);
|
|
9220
|
+
return {
|
|
9221
|
+
deviceCode: enrollment.deviceCode,
|
|
9222
|
+
userCode: enrollment.userCode,
|
|
9223
|
+
expiresAt: new Date(enrollment.expiresAt).toISOString(),
|
|
9224
|
+
intervalMs: this.pollIntervalMs
|
|
9225
|
+
};
|
|
9226
|
+
}
|
|
9227
|
+
/**
|
|
9228
|
+
* Where the runner's enrollment stands, or null once it is unknown or expired. An approval or a
|
|
9229
|
+
* denial is told once and then forgotten, so the token never travels twice.
|
|
9230
|
+
*/
|
|
9231
|
+
poll(deviceCode) {
|
|
9232
|
+
this.prune();
|
|
9233
|
+
const enrollment = this.enrollments.get(deviceCode);
|
|
9234
|
+
if (enrollment === void 0) {
|
|
9235
|
+
return null;
|
|
9236
|
+
}
|
|
9237
|
+
if (enrollment.status.status !== "pending") {
|
|
9238
|
+
this.enrollments.delete(deviceCode);
|
|
9239
|
+
}
|
|
9240
|
+
return enrollment.status;
|
|
9241
|
+
}
|
|
9242
|
+
/** The enrollments waiting for the user, newest first. */
|
|
9243
|
+
waiting() {
|
|
9244
|
+
this.prune();
|
|
9245
|
+
return [...this.enrollments.values()].filter((each) => each.status.status === "pending").toSorted((a, b) => b.requestedAt - a.requestedAt).map((each) => ({
|
|
9246
|
+
userCode: each.userCode,
|
|
9247
|
+
hostname: each.request.hostname,
|
|
9248
|
+
os: each.request.os,
|
|
9249
|
+
clis: each.request.clis,
|
|
9250
|
+
capabilities: each.request.capabilities,
|
|
9251
|
+
version: each.request.version,
|
|
9252
|
+
requestedAt: new Date(each.requestedAt).toISOString(),
|
|
9253
|
+
expiresAt: new Date(each.expiresAt).toISOString()
|
|
9254
|
+
}));
|
|
9255
|
+
}
|
|
9256
|
+
/** Registers the runner a waiting enrollment asked for, under `name`, for its next poll. */
|
|
9257
|
+
async approve(actor, code, name) {
|
|
9258
|
+
const enrollment = this.waitingFor(code);
|
|
9259
|
+
const { runner, token } = await this.board.addRunner(actor, name, {
|
|
9260
|
+
hostname: enrollment.request.hostname
|
|
9261
|
+
});
|
|
9262
|
+
enrollment.status = { status: "approved", name: runner.name, token };
|
|
9263
|
+
return runner;
|
|
9264
|
+
}
|
|
9265
|
+
deny(code) {
|
|
9266
|
+
this.waitingFor(code).status = { status: "denied" };
|
|
9267
|
+
}
|
|
9268
|
+
waitingFor(code) {
|
|
9269
|
+
this.prune();
|
|
9270
|
+
const wanted = normalized(code);
|
|
9271
|
+
for (const enrollment of this.enrollments.values()) {
|
|
9272
|
+
if (enrollment.status.status === "pending" && normalized(enrollment.userCode) === wanted) {
|
|
9273
|
+
return enrollment;
|
|
9274
|
+
}
|
|
9275
|
+
}
|
|
9276
|
+
throw new BoardError("NOT_FOUND", `no runner waits for approval with the code ${code}`);
|
|
9277
|
+
}
|
|
9278
|
+
prune() {
|
|
9279
|
+
for (const [deviceCode, enrollment] of this.enrollments) {
|
|
9280
|
+
if (enrollment.expiresAt <= this.now()) {
|
|
9281
|
+
this.enrollments.delete(deviceCode);
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9284
|
+
}
|
|
9285
|
+
};
|
|
9286
|
+
|
|
8789
9287
|
// apps/server/dist/models.js
|
|
8790
9288
|
var HOUR = 60 * 6e4;
|
|
8791
9289
|
var ModelCatalog = class {
|
|
@@ -8918,10 +9416,23 @@ var app = createApp({
|
|
|
8918
9416
|
scheduler: view,
|
|
8919
9417
|
models: new ModelCatalog((cli) => runners.models(cli)),
|
|
8920
9418
|
runners,
|
|
8921
|
-
webDir
|
|
9419
|
+
webDir,
|
|
9420
|
+
signIn: { github: config.github, log },
|
|
9421
|
+
enrollments: new EnrollmentDesk(board),
|
|
9422
|
+
trustedProxies: config.trustedProxies,
|
|
9423
|
+
log
|
|
8922
9424
|
});
|
|
8923
9425
|
var server = serve({ fetch: app.fetch, port: config.port, hostname: config.host }, (info) => {
|
|
8924
|
-
log.info({
|
|
9426
|
+
log.info({
|
|
9427
|
+
host: info.address,
|
|
9428
|
+
port: info.port,
|
|
9429
|
+
dataDir: config.dataDir,
|
|
9430
|
+
mcpUrl,
|
|
9431
|
+
webDir,
|
|
9432
|
+
webBuilt,
|
|
9433
|
+
signIn: config.github === null ? "token" : `github (${config.github.users.join(", ")})`,
|
|
9434
|
+
trustedProxies: config.trustedProxies
|
|
9435
|
+
}, "board server listening");
|
|
8925
9436
|
});
|
|
8926
9437
|
await scheduler.start();
|
|
8927
9438
|
log.info({
|