codeden 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/cli.js +1 -1
- package/dist/{main-DgpYK_Si.js → main-Bubch4fJ.js} +2 -2
- package/dist/{main-DgpYK_Si.js.map → main-Bubch4fJ.js.map} +1 -1
- package/dist/{main-BAX1sPiD.js → main-f5CBW7t4.js} +1196 -198
- package/dist/main-f5CBW7t4.js.map +1 -0
- package/dist/{runner-DGomZl80.js → runner-Doy0yaNT.js} +198 -3
- package/dist/runner-Doy0yaNT.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/{core-D2h_xdbh.js → core-BnICSmTX.js} +1 -1
- package/dist/web/assets/engine-oniguruma-Tz65MLLK.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-DpId3Lg2.js → highlighted-body-JLMTFACH-DiAsPNHI.js} +1 -1
- package/dist/web/assets/{highlights-C1SeynWZ.js → highlights-mfetlXdJ.js} +1 -1
- package/dist/web/assets/index-CXjq9NOS.js +914 -0
- package/dist/web/assets/index-DcErU_f1.css +1 -0
- package/dist/web/assets/langs-B69Lxr5B.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-CbtYONW-.js +1 -0
- package/dist/web/assets/{shiki-h6vxFpD2.js → shiki-B1ZU2vss.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-D8Euixi9.js → stream-tokenizer-1i17e6mT.js} +1 -1
- package/dist/web/assets/{themes-Bi1mPEwQ.js → themes-CmE23Ko0.js} +2 -2
- package/dist/web/icons/icon-192.png +0 -0
- package/dist/web/icons/icon-512.png +0 -0
- package/dist/web/icons/maskable-192.png +0 -0
- package/dist/web/icons/maskable-512.png +0 -0
- package/dist/web/index.html +10 -2
- package/dist/web/manifest.webmanifest +28 -0
- package/package.json +1 -1
- package/dist/main-BAX1sPiD.js.map +0 -1
- package/dist/runner-DGomZl80.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-CG0oBFVI.js +0 -1
- package/dist/web/assets/index-BPuGYbOr.css +0 -1
- package/dist/web/assets/index-BXhyz2mO.js +0 -914
- package/dist/web/assets/langs-NwkcQ-R2.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-jqhY_qSL.js +0 -1
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as BrowserNavigateRequest, $n as AgentConfigLocator, $t as NOTICE_BATCH_CODE, A as SessionFileQuery, An as GitCommitRequest, At as RespondRequest, B as NetworkStatus, Bn as ConflictDecision, Bt as HealthResponse, C as isElectronRuntime, Cn as AutomationRun, Cr as string, Ct as CreateProjectRequest, D as CODEDEN_VERSION, Dn as SessionStatus, Dr as toJSONSchema, Dt as ListSessionsQuery, E as wrapper_default, En as Session, Er as ZodError, Et as ListInboxQuery, F as ListConflictsQuery, Fn as TerminalClientMessage, Ft as StartSessionRequest, G as SetNetworkPasswordResponse, Gn as DEFAULT_VISION_PATH, Gt as SttQuery, H as RemoteAccessFile, Hn as WorkKey, Ht as Settings, I as ListNoticesQuery, In as TerminalQuery, It as SteerRequest, J as ImageTestRequest, Jn as InboxItemKind, Jt as UpdateInstallRequest, K as SignInRequest, Kn as FactoryLimits, Kt as TtsRequest, L as ResolveConflictRequest, Ln as RunnerCommandResults, Lt as ThreadItemsQuery, M as RunnerToServerMessage, Mn as RestoreFilesRequest, Mt as SendMessageRequest, N as ServerLock, Nn as SessionResult, Nt as SessionStartMeta, O as UsageQuery, On as DiffScope, Or as safeParse$1, Ot as ReorderProjectsRequest, P as ArchiveSessionRequest, Pn as SpeechSummaryRequest, Pt as SetSecretRequest, Q as BrowserHistoryRequest, Qn as AgentConfigListQuery, Qt as ConflictRecord, R as RoleMessageRequest, Rn as SessionOptions, Rt as UiClientMessage, S as acquireInstallLock, Sn as AutomationGuards, Sr as record, St as UpdateAutomationRequest, T as import_websocket_server, Tn as TriagePolicy, Tr as unknown, Tt as FileWriteRequest, U as SetNetworkAccessRequest, Un as Project, Ut as LOCAL_STT_SAMPLE_RATE, V as PASSWORD_RULE_TEXT, Vn as StartedBy, Vt as agentEventToNewEvent, W as SetNetworkPasswordRequest, Wn as sortProjects, Wt as LocalSttModelParams, X as BrowserClientMessage, Xn as AgentConfigDeleteQuery, Xt as compareVersions, Y as summarizeActivity, Yn as messageFrom, Yt as UpdateStatus, Z as BrowserDevServerRequest, Zn as AgentConfigDiffRequest, Zt as isNewerVersion, _ as importClaudeSdkFrom, _n as SetAutonomyModeRequest, _r as looseObject, _t as StarterSetRequest, a as Hono, an as GithubPr, ar as ProviderId, at as AutomationImportSource, b as runNpm, bn as Automation, br as optional, bt as UpcomingRunsQuery, c as createAdaptorServer, cn as githubPrId, cr as _enum, ct as ImportAutomationsRequest, d as createImageEngineRegistry, dn as FactoryCandidate, dr as boolean, dt as AutomationProjectRequest, en as CreateGithubIssueRequest, er as AgentConfigWriteRequest, et as BrowserNewTabRequest, f as IMAGE_ASPECT_RATIOS, fn as FactoryTriageRequest, fr as custom, ft as CreateAutomationRequest, g as CLAUDE_SDK_VERSION, gn as ReviewVerdict, gr as literal, gt as SetAutomationEnabledRequest, h as claudeSdkSource, hn as PutVisionRequest, hr as intersection, ht as RunAutomationRequest, i as killTree$1, in as GithubListQuery, ir as AutonomyLevel, it as AUTOMATION_IMPORT_LIMITS, j as RunnerLock, jn as GitPushRequest, jt as SecretKey, k as UsageSettings, kn as EditQueuedMessageRequest, kr as prettifyError, kt as ResolveInboxRequest, l as upgradeWebSocket, ln as parseGithubPrId, lr as _null, lt as AutomationFileFrontmatter, m as ImageGenerationError, mn as KillSwitchRequest, mr as int, mt as ListAutomationsQuery, n as readLock, nn as GithubIssue, nr as McpServerTarget, nt as BrowserViewportRequest, o as tryDecodeURIComponent, on as MergePrRequest, or as ReasoningEffort, ot as DuplicateAutomationRequest, p as resolveImageOutput, pn as FactoryTuning, pr as discriminatedUnion, pt as ListAutomationRunsQuery, q as passwordProblem, qn as InboxItem, qt as AppPointer, r as isAlive, rn as GithubLabelRequest, rr as McpServerUpsertRequest, rt as BrowserWsQuery, s as bufferToFormData, sn as TriageIssueRequest, sr as datetime, st as ExportAutomationsQuery, tn as GithubCommentRequest, tr as McpServerDiffRequest, tt as BrowserOpenRequest, u as ImageEngineRegistry, un as DiscoveryArea, ur as array, ut as AutomationGuardsPatch, v as installClaudeSdk, vn as isFactoryMode, vr as number, vt as TriageDecisionRequest, w as ownNodeEnv, wn as AutomationTrigger, wr as union$1, wt as FileReadQuery, x as resolveNpm, xn as AutomationAction, xr as preprocess, xt as UpdateAutomationProjectSettingsRequest, y as isClaudeSdkInstalled, yn as Artifact, yr as object, yt as TriageReport, z as UpdateSessionRequest, zn as ProjectConfigFile, zt as UpdateProjectRequest } from "./runner-Doy0yaNT.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import fs, { accessSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import fs, { accessSync, appendFileSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
6
6
|
import { basename, delimiter, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { isDeepStrictEqual, parseArgs } from "node:util";
|
|
8
|
-
import crypto$1, { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
|
+
import crypto$1, { createHash, randomBytes, randomInt, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
10
|
import { gunzipSync } from "node:zlib";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
11
12
|
import { open, rename } from "node:fs/promises";
|
|
12
13
|
import { Readable } from "node:stream";
|
|
13
14
|
import { DatabaseSync } from "node:sqlite";
|
|
15
|
+
import { createServer } from "node:net";
|
|
14
16
|
import { createRequire as createRequire$1 } from "module";
|
|
15
17
|
import { versions } from "node:process";
|
|
16
18
|
// ../../packages/contracts/src/vision.ts
|
|
@@ -120,6 +122,368 @@ function parseProtectedAreas(lines) {
|
|
|
120
122
|
topics: [...topics]
|
|
121
123
|
};
|
|
122
124
|
}
|
|
125
|
+
// ../server/src/network/addresses.ts
|
|
126
|
+
/** `::ffff:192.168.1.2` -> `192.168.1.2`; other addresses unchanged (lower case). */
|
|
127
|
+
function normalizeAddress(address) {
|
|
128
|
+
const lower = (address ?? "").trim().toLowerCase();
|
|
129
|
+
return lower.startsWith("::ffff:") && lower.includes(".") ? lower.slice(7) : lower;
|
|
130
|
+
}
|
|
131
|
+
function ipv4Octets(address) {
|
|
132
|
+
const parts = address.split(".");
|
|
133
|
+
if (parts.length !== 4) return void 0;
|
|
134
|
+
const octets = parts.map((p) => /^\d{1,3}$/.test(p) ? Number(p) : NaN);
|
|
135
|
+
return octets.every((o) => o >= 0 && o <= 255) ? octets : void 0;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Is this peer on this machine? `127.0.0.0/8`, `::1` and their IPv4-mapped forms are; an empty
|
|
139
|
+
* or unknown address is not (fail closed: it counts as remote).
|
|
140
|
+
*/
|
|
141
|
+
function isLoopbackAddress(address) {
|
|
142
|
+
const normalized = normalizeAddress(address);
|
|
143
|
+
if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
|
|
144
|
+
return ipv4Octets(normalized)?.[0] === 127;
|
|
145
|
+
}
|
|
146
|
+
/** Tailscale (and other CGNAT VPNs) hand out addresses in `100.64.0.0/10`. */
|
|
147
|
+
function isVpnRange(address) {
|
|
148
|
+
const octets = ipv4Octets(address);
|
|
149
|
+
return octets !== void 0 && octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127;
|
|
150
|
+
}
|
|
151
|
+
/** Link-local `169.254.0.0/16` (no DHCP lease): never useful to a phone. */
|
|
152
|
+
function isLinkLocal(address) {
|
|
153
|
+
const octets = ipv4Octets(address);
|
|
154
|
+
return octets?.[0] === 169 && octets[1] === 254;
|
|
155
|
+
}
|
|
156
|
+
/** Names of VPN interfaces, recognised so their address is labelled as a VPN. */
|
|
157
|
+
const VPN_NAME = /tailscale|wireguard|wg\d|utun|zerotier|\bzt|tun\d|vpn/i;
|
|
158
|
+
/**
|
|
159
|
+
* Host-only virtual adapters (Hyper-V/WSL, Docker, VirtualBox, VMware, libvirt): no phone can
|
|
160
|
+
* reach them, so they are neither listed nor accepted in `Host`/`Origin`.
|
|
161
|
+
*/
|
|
162
|
+
const VIRTUAL_NAME = /^(vethernet|docker\d*|br-|veth|virbr|vmnet|vboxnet)|virtualbox|vmware|hyper-v/i;
|
|
163
|
+
/**
|
|
164
|
+
* Every address CodeDen answers on for `port`: this machine (`localhost`) first, then each
|
|
165
|
+
* non-internal IPv4 address, LAN before VPN, leaving out host-only virtual adapters. Tailscale's range, and interfaces named like a VPN,
|
|
166
|
+
* count as `vpn`. Interfaces are injectable for tests.
|
|
167
|
+
*/
|
|
168
|
+
function listAddresses(port, interfaces = networkInterfaces()) {
|
|
169
|
+
const local = {
|
|
170
|
+
kind: "local",
|
|
171
|
+
label: "This computer",
|
|
172
|
+
interfaceName: null,
|
|
173
|
+
host: "localhost",
|
|
174
|
+
url: `http://localhost:${port}`
|
|
175
|
+
};
|
|
176
|
+
const found = [];
|
|
177
|
+
const seen = /* @__PURE__ */ new Set();
|
|
178
|
+
for (const [name, infos] of Object.entries(interfaces)) for (const info of infos ?? []) {
|
|
179
|
+
if (info.family !== "IPv4" || info.internal) continue;
|
|
180
|
+
const host = info.address;
|
|
181
|
+
if (seen.has(host) || isLinkLocal(host) || isLoopbackAddress(host)) continue;
|
|
182
|
+
if (VIRTUAL_NAME.test(name) && !isVpnRange(host)) continue;
|
|
183
|
+
seen.add(host);
|
|
184
|
+
const vpn = isVpnRange(host) || VPN_NAME.test(name);
|
|
185
|
+
const tailscale = isVpnRange(host) || /tailscale/i.test(name);
|
|
186
|
+
found.push({
|
|
187
|
+
kind: vpn ? "vpn" : "lan",
|
|
188
|
+
label: tailscale ? "Tailscale" : vpn ? `VPN (${name})` : name,
|
|
189
|
+
interfaceName: name,
|
|
190
|
+
host,
|
|
191
|
+
url: `http://${host}:${port}`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
found.sort((a, b) => Number(a.kind === "vpn") - Number(b.kind === "vpn"));
|
|
195
|
+
return [local, ...found];
|
|
196
|
+
}
|
|
197
|
+
// ../server/src/network/words.ts
|
|
198
|
+
/**
|
|
199
|
+
* Short, common, easy-to-type English words for generated network-access passphrases
|
|
200
|
+
* (`generatePassphrase`). Lower case, 3 to 9 letters, no duplicates (a test keeps it so).
|
|
201
|
+
*/
|
|
202
|
+
const PASSPHRASE_WORDS = `
|
|
203
|
+
able acid acorn actor adapt adult agent agree ahead aim air alarm album alert alien alley
|
|
204
|
+
alpine amber amount anchor angle ankle answer apple april apron arch arena argue arm armor
|
|
205
|
+
army arrow art ash aspen atlas atom attic audio aunt autumn avenue award awake axis bacon
|
|
206
|
+
badge bag bake balcony ball bamboo banana band bank barn barrel basil basket bath beach
|
|
207
|
+
beam bean bear beard beat bee beetle bell belt bench berry bike bird biscuit bishop bitter
|
|
208
|
+
blade blank blast blaze blend blink block bloom blue board boat body bolt bone bonus book
|
|
209
|
+
boost boot border bottle bounce bowl box brain branch brass brave bread breeze brick bridge
|
|
210
|
+
brief bright brisk broom brush bubble bucket buddy budget buffalo bugle build bulb bundle
|
|
211
|
+
bunny burger burst bus butter button cabin cable cactus cake calm camel camera camp canal
|
|
212
|
+
candle candy canoe canvas canyon cape captain car carbon card cargo carpet carrot cart case
|
|
213
|
+
castle cat cave cedar cello cereal chain chair chalk champ chapel charm chart chase cheese
|
|
214
|
+
chef cherry chess chest chick chief child chili chimney chin chip choir chorus cider cinema
|
|
215
|
+
circle city clam clap clay clerk cliff climb clock cloud clover clown club coach coal coast
|
|
216
|
+
coat cobra cocoa coconut code coffee coin collar color comet comic compass cookie copper
|
|
217
|
+
coral cord corn cottage cotton couch cousin cove cow crab craft crane crater crayon cream
|
|
218
|
+
creek crew cricket crisp crop cross crowd crown crumb crystal cube cup curtain curve cushion
|
|
219
|
+
cycle daisy dance dawn deck deer delta denim depth desert desk dial diary diesel dinner
|
|
220
|
+
dish diver dock doctor dog dollar dolphin donkey donut door dove dragonfly drama drawer
|
|
221
|
+
dream dress drift drill drum duck dune dusk dust eagle earth easel echo eclipse edge eel
|
|
222
|
+
egg elbow elder elk ember empty engine entry envoy epic equal error essay estate ethic
|
|
223
|
+
event exit fabric falcon fame family fancy farm feast feather fence fern ferry festival
|
|
224
|
+
fiber field fig film finch finger fire fish flag flame flash fleet flint float flock flood
|
|
225
|
+
floor flour flower flute focus fog foil folk forest forge fork fossil fox frame frost fruit
|
|
226
|
+
fudge fuel gadget galaxy garden garlic gate gauge gecko gem genre ghost giant ginger giraffe
|
|
227
|
+
glass glide globe glove glow glue goat gold golf goose gopher gospel grain granite grape
|
|
228
|
+
graph grass gravel gravy green grid grill grove guitar gulf gym habit hammer hamster hand
|
|
229
|
+
harbor harp harvest hat hawk hazel heart hedge helmet hen herb hero heron hill hinge hippo
|
|
230
|
+
hobby hockey honey hood hook horizon horn horse hotel hound house hub humor hut iceberg
|
|
231
|
+
icon idea igloo inch index ink input insect iron island ivory ivy jacket jaguar jam jar
|
|
232
|
+
jazz jelly jet jewel jigsaw jockey judge juice jungle kayak kernel kettle key kidney king
|
|
233
|
+
kite kitten kiwi knee knife knot koala label ladder lagoon lake lamb lamp lantern laptop
|
|
234
|
+
lark laser latch lava lawn layer leaf ledge lemon lens leopard letter lever library lily
|
|
235
|
+
lime linen lion lizard llama lobster locket lodge logic lotus lunar lunch lynx magnet
|
|
236
|
+
mango manor maple marble market marsh mask meadow medal melon memory mentor menu metal
|
|
237
|
+
meteor mint mirror mitten moat model mole mongoose moon moose mosaic moss moth motor
|
|
238
|
+
mountain mouse muffin mule mural museum mushroom music mustard nail napkin native nebula
|
|
239
|
+
needle nest net nickel noble noodle north notch novel nugget nurse nutmeg oak oasis ocean
|
|
240
|
+
octopus olive omelet onion opal opera orange orbit orchard orchid organ otter outlet oval
|
|
241
|
+
oven owl oyster paddle page paint palace palm panda panel panther paper parade parcel park
|
|
242
|
+
parrot pasta patch path peach peanut pear pebble pecan pedal pelican pencil pepper piano
|
|
243
|
+
pickle picnic pigeon pillow pilot pine pirate pizza planet plank plaza plum pocket poem
|
|
244
|
+
polar pond pony poppy porch potato pottery powder prairie prism prize puddle pulse pumpkin
|
|
245
|
+
puppet puppy puzzle python quail quartz queen quest quill quilt quiz rabbit raccoon radar
|
|
246
|
+
radio raft rain raisin rake ranch raven razor recipe reef relay ribbon rice ridge rifle
|
|
247
|
+
ring river road robin robot rocket rodeo roof rook rope rose rover ruby rudder rug ruler
|
|
248
|
+
saddle safari saga sail salad salmon salt sand sandal satin saucer sauna scale scarf school
|
|
249
|
+
scooter scout screen scroll seal season seed shark sheep shelf shell sherpa shield ship
|
|
250
|
+
shirt shoe shore shovel shrimp signal silk silver singer siren sketch ski skunk sky sled
|
|
251
|
+
slope sloth smile snail snake snow soap soccer sock sofa solar soup spark sparrow spear
|
|
252
|
+
sphere spice spider spinach spoon spring sprout spruce squad squid stable stadium stamp
|
|
253
|
+
star statue steam steel stem stereo stone stool storm stove straw stream street string
|
|
254
|
+
studio sugar suit summit sun surf swamp swan sweater swing sword syrup table tablet taco
|
|
255
|
+
tail talent tango tank tea teapot temple tennis tent thistle thorn thread thunder ticket
|
|
256
|
+
tide tiger timber toast toffee tomato tonic topaz torch tornado tortoise towel tower toy
|
|
257
|
+
track tractor trail train tram tree trick trolley trophy trout truck trumpet tulip tuna
|
|
258
|
+
tunnel turkey turnip turtle tutor twig umbrella unicorn urban vacuum valley valve van vase
|
|
259
|
+
velvet vessel violet violin visor voice volcano voyage waffle wagon walnut walrus wand
|
|
260
|
+
wave wax weasel whale wheat wheel whistle willow window winter wizard wolf wombat wood
|
|
261
|
+
wool yacht yak yard yarn yeti yogurt zebra zero zigzag zinc zipper zone
|
|
262
|
+
`.trim().split(/\s+/);
|
|
263
|
+
// ../server/src/network/password.ts
|
|
264
|
+
/** scrypt cost: N=2^15, r=8, p=1 (about 32 MiB and tens of milliseconds per hash). */
|
|
265
|
+
const DEFAULT_COST = {
|
|
266
|
+
N: 2 ** 15,
|
|
267
|
+
r: 8,
|
|
268
|
+
p: 1
|
|
269
|
+
};
|
|
270
|
+
const KEY_LENGTH = 32;
|
|
271
|
+
const SALT_BYTES = 16;
|
|
272
|
+
function scryptAsync(password, salt, opts) {
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
const maxmem = 256 * (opts.N ?? DEFAULT_COST.N) * (opts.r ?? DEFAULT_COST.r) + 1048576;
|
|
275
|
+
scrypt(password.normalize("NFKC"), salt, KEY_LENGTH, {
|
|
276
|
+
...opts,
|
|
277
|
+
maxmem
|
|
278
|
+
}, (error, key) => error ? reject(error) : resolve(key));
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Hash a network-access password with scrypt and a random salt:
|
|
283
|
+
* `scrypt$<N>$<r>$<p>$<salt b64>$<hash b64>`. The password itself is never stored.
|
|
284
|
+
*/
|
|
285
|
+
async function hashPassword(password, cost = DEFAULT_COST) {
|
|
286
|
+
const salt = randomBytes(SALT_BYTES);
|
|
287
|
+
const key = await scryptAsync(password, salt, cost);
|
|
288
|
+
return [
|
|
289
|
+
"scrypt",
|
|
290
|
+
cost.N,
|
|
291
|
+
cost.r,
|
|
292
|
+
cost.p,
|
|
293
|
+
salt.toString("base64"),
|
|
294
|
+
key.toString("base64")
|
|
295
|
+
].join("$");
|
|
296
|
+
}
|
|
297
|
+
/** Check a password against a stored hash in constant time; false for malformed hashes. */
|
|
298
|
+
async function verifyPassword(password, stored) {
|
|
299
|
+
const parts = stored.split("$");
|
|
300
|
+
if (parts.length !== 6 || parts[0] !== "scrypt") return false;
|
|
301
|
+
const [N, r, p] = parts.slice(1, 4).map(Number);
|
|
302
|
+
if (!N || !r || !p || N > 2 ** 20 || r > 32 || p > 16) return false;
|
|
303
|
+
const salt = Buffer.from(parts[4], "base64");
|
|
304
|
+
const expected = Buffer.from(parts[5], "base64");
|
|
305
|
+
if (expected.length !== KEY_LENGTH) return false;
|
|
306
|
+
try {
|
|
307
|
+
const key = await scryptAsync(password, salt, {
|
|
308
|
+
N,
|
|
309
|
+
r,
|
|
310
|
+
p
|
|
311
|
+
});
|
|
312
|
+
return timingSafeEqual(key, expected);
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A random passphrase of five words from `PASSPHRASE_WORDS`, joined with dashes (about 49
|
|
319
|
+
* bits; always longer than the 15-character minimum). Drawn again in the rare case the rules
|
|
320
|
+
* refuse it.
|
|
321
|
+
*/
|
|
322
|
+
function generatePassphrase(pick = randomInt) {
|
|
323
|
+
for (;;) {
|
|
324
|
+
const phrase = Array.from({ length: 5 }, () => PASSPHRASE_WORDS[pick(PASSPHRASE_WORDS.length)]).join("-");
|
|
325
|
+
if (!passwordProblem(phrase)) return phrase;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ../server/src/json-file.ts
|
|
329
|
+
/** Read and parse a JSON file; `undefined` when missing or unparsable. */
|
|
330
|
+
function readJsonFile(file) {
|
|
331
|
+
try {
|
|
332
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
333
|
+
} catch {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Write JSON atomically (temp file + rename) so readers never see a partial file. */
|
|
338
|
+
function writeJsonFile(file, value, mode) {
|
|
339
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
340
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
341
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, mode ? { mode } : void 0);
|
|
342
|
+
renameSync(tmp, file);
|
|
343
|
+
}
|
|
344
|
+
// ../server/src/network/store.ts
|
|
345
|
+
/** Prefix of remote session tokens, so they are recognisable in a cookie jar. */
|
|
346
|
+
const TOKEN_PREFIX = "cds_";
|
|
347
|
+
const SESSION_MS = 7776e6;
|
|
348
|
+
/** `lastSeenAt` is written to disk at most this often per session. */
|
|
349
|
+
const SEEN_WRITE_MS = 6e4;
|
|
350
|
+
const LABEL_MAX = 200;
|
|
351
|
+
function sha256$3(token) {
|
|
352
|
+
return createHash("sha256").update(token).digest("hex");
|
|
353
|
+
}
|
|
354
|
+
/** A password the rules refuse, with the reason in plain words. */
|
|
355
|
+
var PasswordRuleError = class extends Error {
|
|
356
|
+
code = "weak_password";
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* `<home>/remote-access.json` (§15.1): the network switch, the password's scrypt hash and the
|
|
360
|
+
* remote sessions (hashes of their tokens only). Shared by the server and the `codeden
|
|
361
|
+
* password` / `codeden network` commands when no server is running.
|
|
362
|
+
*/
|
|
363
|
+
var RemoteAccessStore = class {
|
|
364
|
+
file;
|
|
365
|
+
now;
|
|
366
|
+
data;
|
|
367
|
+
seenWritten = /* @__PURE__ */ new Map();
|
|
368
|
+
constructor(file, now = Date.now) {
|
|
369
|
+
this.file = file;
|
|
370
|
+
this.now = now;
|
|
371
|
+
this.data = this.load();
|
|
372
|
+
}
|
|
373
|
+
get enabled() {
|
|
374
|
+
return this.data.enabled;
|
|
375
|
+
}
|
|
376
|
+
get passwordSet() {
|
|
377
|
+
return this.data.password !== null;
|
|
378
|
+
}
|
|
379
|
+
get passwordSetAt() {
|
|
380
|
+
return this.data.password?.setAt ?? null;
|
|
381
|
+
}
|
|
382
|
+
/** Re-read the file (the CLI may have changed it while the server was stopped). */
|
|
383
|
+
reload() {
|
|
384
|
+
this.data = this.load();
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Set the password after checking the rules (throws `PasswordRuleError`), and sign out every
|
|
388
|
+
* remote device.
|
|
389
|
+
*/
|
|
390
|
+
async setPassword(password) {
|
|
391
|
+
const problem = passwordProblem(password);
|
|
392
|
+
if (problem) throw new PasswordRuleError(problem);
|
|
393
|
+
const hash = await hashPassword(password);
|
|
394
|
+
this.data = {
|
|
395
|
+
...this.data,
|
|
396
|
+
password: {
|
|
397
|
+
hash,
|
|
398
|
+
setAt: new Date(this.now()).toISOString()
|
|
399
|
+
},
|
|
400
|
+
sessions: []
|
|
401
|
+
};
|
|
402
|
+
this.save();
|
|
403
|
+
}
|
|
404
|
+
async checkPassword(password) {
|
|
405
|
+
const stored = this.data.password?.hash;
|
|
406
|
+
return stored ? verifyPassword(password, stored) : false;
|
|
407
|
+
}
|
|
408
|
+
/** Turn network access on or off. Turning it on without a password throws. */
|
|
409
|
+
setEnabled(enabled) {
|
|
410
|
+
if (enabled && !this.passwordSet) throw new Error("Set a network access password before turning network access on.");
|
|
411
|
+
this.data = {
|
|
412
|
+
...this.data,
|
|
413
|
+
enabled
|
|
414
|
+
};
|
|
415
|
+
this.save();
|
|
416
|
+
}
|
|
417
|
+
/** Start a 90-day session for a device; returns the token for its cookie (stored hashed). */
|
|
418
|
+
createSession(device) {
|
|
419
|
+
const token = `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
|
|
420
|
+
const now = this.now();
|
|
421
|
+
const session = {
|
|
422
|
+
id: randomUUID(),
|
|
423
|
+
tokenHash: sha256$3(token),
|
|
424
|
+
label: device.label.slice(0, LABEL_MAX) || "Unknown device",
|
|
425
|
+
address: device.address,
|
|
426
|
+
createdAt: new Date(now).toISOString(),
|
|
427
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
428
|
+
expiresAt: new Date(now + SESSION_MS).toISOString()
|
|
429
|
+
};
|
|
430
|
+
this.data = {
|
|
431
|
+
...this.data,
|
|
432
|
+
sessions: [...this.liveSessions(), session]
|
|
433
|
+
};
|
|
434
|
+
this.save();
|
|
435
|
+
return {
|
|
436
|
+
token,
|
|
437
|
+
session
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/** The live session for a cookie token (and note that it was seen), else undefined. */
|
|
441
|
+
verifySession(token) {
|
|
442
|
+
if (!token?.startsWith(TOKEN_PREFIX)) return void 0;
|
|
443
|
+
const hash = sha256$3(token);
|
|
444
|
+
const now = this.now();
|
|
445
|
+
const session = this.data.sessions.find((s) => s.tokenHash === hash);
|
|
446
|
+
if (!session || Date.parse(session.expiresAt) <= now) return void 0;
|
|
447
|
+
session.lastSeenAt = new Date(now).toISOString();
|
|
448
|
+
if (now - (this.seenWritten.get(session.id) ?? 0) >= SEEN_WRITE_MS) {
|
|
449
|
+
this.seenWritten.set(session.id, now);
|
|
450
|
+
this.save();
|
|
451
|
+
}
|
|
452
|
+
return session;
|
|
453
|
+
}
|
|
454
|
+
/** Unexpired sessions, oldest first. */
|
|
455
|
+
liveSessions() {
|
|
456
|
+
const now = this.now();
|
|
457
|
+
return this.data.sessions.filter((s) => Date.parse(s.expiresAt) > now);
|
|
458
|
+
}
|
|
459
|
+
/** Sign out one device; false when there was no such session. */
|
|
460
|
+
revoke(id) {
|
|
461
|
+
const before = this.data.sessions.length;
|
|
462
|
+
this.data = {
|
|
463
|
+
...this.data,
|
|
464
|
+
sessions: this.data.sessions.filter((s) => s.id !== id)
|
|
465
|
+
};
|
|
466
|
+
if (this.data.sessions.length === before) return false;
|
|
467
|
+
this.save();
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
/** Sign out every device. */
|
|
471
|
+
revokeAll() {
|
|
472
|
+
this.data = {
|
|
473
|
+
...this.data,
|
|
474
|
+
sessions: []
|
|
475
|
+
};
|
|
476
|
+
this.save();
|
|
477
|
+
}
|
|
478
|
+
load() {
|
|
479
|
+
if (!existsSync(this.file)) return RemoteAccessFile.parse({});
|
|
480
|
+
const parsed = RemoteAccessFile.safeParse(readJsonFile(this.file) ?? {});
|
|
481
|
+
return parsed.success ? parsed.data : RemoteAccessFile.parse({});
|
|
482
|
+
}
|
|
483
|
+
save() {
|
|
484
|
+
writeJsonFile(this.file, this.data, 384);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
123
487
|
// ../server/src/paths.ts
|
|
124
488
|
/**
|
|
125
489
|
* Resolve the data directory (§6): explicit value, then `CODEDEN_HOME`, then `~/.codeden`.
|
|
@@ -138,6 +502,10 @@ function homePaths(home) {
|
|
|
138
502
|
db: join(home, "codeden.db"),
|
|
139
503
|
settings: join(home, "settings.json"),
|
|
140
504
|
token: join(home, "install-token"),
|
|
505
|
+
/** Network access switch, password hash and remote sessions (§15.1). */
|
|
506
|
+
remoteAccess: join(home, "remote-access.json"),
|
|
507
|
+
/** Failed remote sign-ins (address and time, never the password). */
|
|
508
|
+
remoteAccessLog: join(home, "logs", "remote-access.log"),
|
|
141
509
|
serverLock: join(home, "server.json"),
|
|
142
510
|
runnerDir: join(home, "runner"),
|
|
143
511
|
runnerLock: join(home, "runner", "runner.json"),
|
|
@@ -8128,22 +8496,6 @@ function ingestRunnerEvent(db, store, runnerId, msg) {
|
|
|
8128
8496
|
ackSeq: msg.seq
|
|
8129
8497
|
};
|
|
8130
8498
|
}
|
|
8131
|
-
// ../server/src/json-file.ts
|
|
8132
|
-
/** Read and parse a JSON file; `undefined` when missing or unparsable. */
|
|
8133
|
-
function readJsonFile(file) {
|
|
8134
|
-
try {
|
|
8135
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
8136
|
-
} catch {
|
|
8137
|
-
return;
|
|
8138
|
-
}
|
|
8139
|
-
}
|
|
8140
|
-
/** Write JSON atomically (temp file + rename) so readers never see a partial file. */
|
|
8141
|
-
function writeJsonFile(file, value, mode) {
|
|
8142
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
8143
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
8144
|
-
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, mode ? { mode } : void 0);
|
|
8145
|
-
renameSync(tmp, file);
|
|
8146
|
-
}
|
|
8147
8499
|
// ../server/src/runner/lock.ts
|
|
8148
8500
|
/** True when a process with this pid exists (EPERM still means it exists). */
|
|
8149
8501
|
function isProcessAlive(pid) {
|
|
@@ -20908,6 +21260,289 @@ function createMcpHandler(deps) {
|
|
|
20908
21260
|
}
|
|
20909
21261
|
};
|
|
20910
21262
|
}
|
|
21263
|
+
// ../server/src/network/rate-limit.ts
|
|
21264
|
+
/** Failures allowed before each attempt has to wait. */
|
|
21265
|
+
const FREE_FAILURES = 3;
|
|
21266
|
+
/** First wait once the free failures are used up; it doubles with each failure. */
|
|
21267
|
+
const BASE_DELAY_MS = 1e3;
|
|
21268
|
+
const MAX_DELAY_MS = 3e4;
|
|
21269
|
+
/** This many failures inside the window lock the address out. */
|
|
21270
|
+
const LOCKOUT_FAILURES = 10;
|
|
21271
|
+
const WINDOW_MS = 9e5;
|
|
21272
|
+
const LOCKOUT_MS = 9e5;
|
|
21273
|
+
/**
|
|
21274
|
+
* Sign-in throttling per client address (§15.1). Only failed attempts count. After three
|
|
21275
|
+
* failures each attempt waits 1 s, 2 s, 4 s … up to 30 s after the last failure; ten failures
|
|
21276
|
+
* within 15 minutes lock the address out for 15 minutes. Other addresses are unaffected, and a
|
|
21277
|
+
* successful sign-in clears the address. Kept in memory: a restart forgets it.
|
|
21278
|
+
*/
|
|
21279
|
+
var SignInLimiter = class SignInLimiter {
|
|
21280
|
+
now;
|
|
21281
|
+
records = /* @__PURE__ */ new Map();
|
|
21282
|
+
constructor(now = Date.now) {
|
|
21283
|
+
this.now = now;
|
|
21284
|
+
}
|
|
21285
|
+
/** Wait required before the `failures`-th failure may be followed by another attempt. */
|
|
21286
|
+
static delayAfter(failures) {
|
|
21287
|
+
if (failures < FREE_FAILURES) return 0;
|
|
21288
|
+
return Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** (failures - FREE_FAILURES));
|
|
21289
|
+
}
|
|
21290
|
+
/** May `address` try a password now? */
|
|
21291
|
+
check(address) {
|
|
21292
|
+
const record = this.prune(address);
|
|
21293
|
+
if (!record) return { allowed: true };
|
|
21294
|
+
const now = this.now();
|
|
21295
|
+
if (record.lockedUntil > now) return {
|
|
21296
|
+
allowed: false,
|
|
21297
|
+
reason: "locked_out",
|
|
21298
|
+
retryAfterMs: record.lockedUntil - now
|
|
21299
|
+
};
|
|
21300
|
+
const last = record.failures.at(-1);
|
|
21301
|
+
const wait = last === void 0 ? 0 : last + SignInLimiter.delayAfter(record.failures.length) - now;
|
|
21302
|
+
return wait > 0 ? {
|
|
21303
|
+
allowed: false,
|
|
21304
|
+
reason: "rate_limited",
|
|
21305
|
+
retryAfterMs: wait
|
|
21306
|
+
} : { allowed: true };
|
|
21307
|
+
}
|
|
21308
|
+
/** Record a wrong password from `address`. */
|
|
21309
|
+
fail(address) {
|
|
21310
|
+
const now = this.now();
|
|
21311
|
+
const record = this.prune(address) ?? {
|
|
21312
|
+
failures: [],
|
|
21313
|
+
lockedUntil: 0
|
|
21314
|
+
};
|
|
21315
|
+
record.failures.push(now);
|
|
21316
|
+
if (record.failures.length >= LOCKOUT_FAILURES) {
|
|
21317
|
+
record.lockedUntil = now + LOCKOUT_MS;
|
|
21318
|
+
record.failures = [];
|
|
21319
|
+
}
|
|
21320
|
+
this.records.set(address, record);
|
|
21321
|
+
}
|
|
21322
|
+
/** A correct password: forget the address's failures. */
|
|
21323
|
+
succeed(address) {
|
|
21324
|
+
this.records.delete(address);
|
|
21325
|
+
}
|
|
21326
|
+
/** Drop failures older than the window; returns the record if anything is left. */
|
|
21327
|
+
prune(address) {
|
|
21328
|
+
const record = this.records.get(address);
|
|
21329
|
+
if (!record) return void 0;
|
|
21330
|
+
const now = this.now();
|
|
21331
|
+
record.failures = record.failures.filter((t) => now - t < WINDOW_MS);
|
|
21332
|
+
if (record.failures.length === 0 && record.lockedUntil <= now) {
|
|
21333
|
+
this.records.delete(address);
|
|
21334
|
+
return;
|
|
21335
|
+
}
|
|
21336
|
+
return record;
|
|
21337
|
+
}
|
|
21338
|
+
};
|
|
21339
|
+
// ../server/src/network/access.ts
|
|
21340
|
+
/** Interface addresses are re-read at most this often (Host/Origin checks run per request). */
|
|
21341
|
+
const ADDRESS_CACHE_MS = 5e3;
|
|
21342
|
+
/**
|
|
21343
|
+
* Network access (§15.1): owns the switch, the password, remote sessions, the sign-in limiter
|
|
21344
|
+
* and where the HTTP listener is bound. Turning access on moves the listener to `0.0.0.0`
|
|
21345
|
+
* (same port); off moves it back to `127.0.0.1` and drops remote connections. The runner is
|
|
21346
|
+
* never involved.
|
|
21347
|
+
*/
|
|
21348
|
+
var NetworkAccess = class {
|
|
21349
|
+
opts;
|
|
21350
|
+
store;
|
|
21351
|
+
limiter;
|
|
21352
|
+
listener;
|
|
21353
|
+
interfaces;
|
|
21354
|
+
log;
|
|
21355
|
+
cached;
|
|
21356
|
+
constructor(opts) {
|
|
21357
|
+
this.opts = opts;
|
|
21358
|
+
this.store = opts.store;
|
|
21359
|
+
this.listener = opts.listener;
|
|
21360
|
+
this.interfaces = opts.interfaces ?? (() => void 0);
|
|
21361
|
+
this.limiter = new SignInLimiter(opts.now);
|
|
21362
|
+
this.log = opts.log ?? (() => {});
|
|
21363
|
+
}
|
|
21364
|
+
/** On only when switched on *and* a password is set: never bind beyond loopback without one. */
|
|
21365
|
+
get enabled() {
|
|
21366
|
+
return this.store.enabled && this.store.passwordSet;
|
|
21367
|
+
}
|
|
21368
|
+
/** Where the listener should be bound for the stored settings. */
|
|
21369
|
+
desiredHost() {
|
|
21370
|
+
return this.enabled ? "0.0.0.0" : "127.0.0.1";
|
|
21371
|
+
}
|
|
21372
|
+
get port() {
|
|
21373
|
+
return this.listener.port;
|
|
21374
|
+
}
|
|
21375
|
+
/** Every address the server would answer on (localhost first), cached briefly. */
|
|
21376
|
+
candidates() {
|
|
21377
|
+
const now = Date.now();
|
|
21378
|
+
const port = this.listener.port;
|
|
21379
|
+
if (!this.cached || this.cached.port !== port || now - this.cached.at > ADDRESS_CACHE_MS) this.cached = {
|
|
21380
|
+
at: now,
|
|
21381
|
+
port,
|
|
21382
|
+
addresses: listAddresses(port, this.interfaces())
|
|
21383
|
+
};
|
|
21384
|
+
return this.cached.addresses;
|
|
21385
|
+
}
|
|
21386
|
+
/** Addresses it answers on now: localhost only while off. */
|
|
21387
|
+
addresses() {
|
|
21388
|
+
const all = this.candidates();
|
|
21389
|
+
return this.listener.host === "0.0.0.0" ? all : all.filter((a) => a.kind === "local");
|
|
21390
|
+
}
|
|
21391
|
+
/** The machine's own LAN/VPN hosts accepted in `Host` and `Origin` while access is on. */
|
|
21392
|
+
ownHosts() {
|
|
21393
|
+
if (!this.enabled || this.listener.host !== "0.0.0.0") return /* @__PURE__ */ new Set();
|
|
21394
|
+
return new Set(this.candidates().filter((a) => a.kind !== "local").map((a) => a.host));
|
|
21395
|
+
}
|
|
21396
|
+
status(caller) {
|
|
21397
|
+
return {
|
|
21398
|
+
enabled: this.enabled,
|
|
21399
|
+
passwordSet: this.store.passwordSet,
|
|
21400
|
+
passwordSetAt: this.store.passwordSetAt,
|
|
21401
|
+
listening: this.listener.host === "0.0.0.0" ? "network" : "loopback",
|
|
21402
|
+
port: this.listener.port,
|
|
21403
|
+
addresses: this.addresses(),
|
|
21404
|
+
candidates: this.candidates(),
|
|
21405
|
+
devices: this.store.liveSessions().map((s) => ({
|
|
21406
|
+
id: s.id,
|
|
21407
|
+
label: s.label,
|
|
21408
|
+
address: s.address,
|
|
21409
|
+
createdAt: s.createdAt,
|
|
21410
|
+
lastSeenAt: s.lastSeenAt,
|
|
21411
|
+
expiresAt: s.expiresAt,
|
|
21412
|
+
current: s.id === caller.sessionId
|
|
21413
|
+
})),
|
|
21414
|
+
remote: caller.remote
|
|
21415
|
+
};
|
|
21416
|
+
}
|
|
21417
|
+
/** Turn network access on (needs a password) or off, and move the listener to match. */
|
|
21418
|
+
async setEnabled(enabled) {
|
|
21419
|
+
this.store.setEnabled(enabled);
|
|
21420
|
+
await this.applyBinding();
|
|
21421
|
+
}
|
|
21422
|
+
/** Bind the listener where the stored settings say; drops remote connections when off. */
|
|
21423
|
+
async applyBinding() {
|
|
21424
|
+
const host = this.desiredHost();
|
|
21425
|
+
await this.listener.rebind(host);
|
|
21426
|
+
this.cached = void 0;
|
|
21427
|
+
if (host === "127.0.0.1") this.listener.dropRemote();
|
|
21428
|
+
this.log(host === "0.0.0.0" ? `network access on: ${this.addresses().map((a) => a.url).join(", ")}` : "network access off: listening on 127.0.0.1 only");
|
|
21429
|
+
}
|
|
21430
|
+
/** Set (or generate) the password; signs out every remote device. Returns a generated one. */
|
|
21431
|
+
async setPassword(input) {
|
|
21432
|
+
const generated = "generate" in input ? generatePassphrase() : null;
|
|
21433
|
+
await this.store.setPassword(generated ?? input.password);
|
|
21434
|
+
this.listener.dropRemote();
|
|
21435
|
+
this.log("network access password changed: every remote device was signed out");
|
|
21436
|
+
return generated;
|
|
21437
|
+
}
|
|
21438
|
+
/** Sign out one device (and close remote connections so it notices). */
|
|
21439
|
+
revoke(id) {
|
|
21440
|
+
const revoked = this.store.revoke(id);
|
|
21441
|
+
if (revoked) this.listener.dropRemote();
|
|
21442
|
+
return revoked;
|
|
21443
|
+
}
|
|
21444
|
+
revokeAll() {
|
|
21445
|
+
this.store.revokeAll();
|
|
21446
|
+
this.listener.dropRemote();
|
|
21447
|
+
}
|
|
21448
|
+
/** Note a failed sign-in with its address (the password is never logged). */
|
|
21449
|
+
recordFailedSignIn(address, reason) {
|
|
21450
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} failed sign-in from ${address || "unknown"}: ${reason}`;
|
|
21451
|
+
this.log(`network: ${line}`);
|
|
21452
|
+
if (this.opts.securityLogFile) try {
|
|
21453
|
+
appendFileSync(this.opts.securityLogFile, `${line}\n`, { mode: 384 });
|
|
21454
|
+
} catch {}
|
|
21455
|
+
}
|
|
21456
|
+
};
|
|
21457
|
+
// ../server/src/network/listener.ts
|
|
21458
|
+
/** How long a rebind keeps retrying while the OS releases the port. */
|
|
21459
|
+
const REBIND_TIMEOUT_MS = 5e3;
|
|
21460
|
+
/**
|
|
21461
|
+
* The TCP listener in front of the HTTP server (§15.1). The HTTP server (with its WebSocket
|
|
21462
|
+
* upgrade handling) never listens itself: this accepts connections and hands them over, so
|
|
21463
|
+
* switching between `127.0.0.1` and `0.0.0.0` swaps only the listening socket. Open
|
|
21464
|
+
* connections, including the UI's WebSockets, carry on, and the runner is untouched.
|
|
21465
|
+
*/
|
|
21466
|
+
var HttpListener = class {
|
|
21467
|
+
http;
|
|
21468
|
+
server;
|
|
21469
|
+
sockets = /* @__PURE__ */ new Set();
|
|
21470
|
+
boundHost = "127.0.0.1";
|
|
21471
|
+
boundPort = 0;
|
|
21472
|
+
constructor(http) {
|
|
21473
|
+
this.http = http;
|
|
21474
|
+
}
|
|
21475
|
+
get host() {
|
|
21476
|
+
return this.boundHost;
|
|
21477
|
+
}
|
|
21478
|
+
get port() {
|
|
21479
|
+
return this.boundPort;
|
|
21480
|
+
}
|
|
21481
|
+
/** Bind for the first time (`port` 0 picks a free port); resolves with the port. */
|
|
21482
|
+
async listen(port, host) {
|
|
21483
|
+
this.server = await this.bind(port, host);
|
|
21484
|
+
this.boundHost = host;
|
|
21485
|
+
const address = this.server.address();
|
|
21486
|
+
this.boundPort = typeof address === "object" && address ? address.port : port;
|
|
21487
|
+
return this.boundPort;
|
|
21488
|
+
}
|
|
21489
|
+
/** Move the listener to `host` on the same port. Open connections stay open. */
|
|
21490
|
+
async rebind(host) {
|
|
21491
|
+
if (host === this.boundHost && this.server) return;
|
|
21492
|
+
this.server?.close();
|
|
21493
|
+
this.server = void 0;
|
|
21494
|
+
const deadline = Date.now() + REBIND_TIMEOUT_MS;
|
|
21495
|
+
for (;;) try {
|
|
21496
|
+
this.server = await this.bind(this.boundPort, host);
|
|
21497
|
+
this.boundHost = host;
|
|
21498
|
+
return;
|
|
21499
|
+
} catch (error) {
|
|
21500
|
+
if (error.code !== "EADDRINUSE" || Date.now() > deadline) {
|
|
21501
|
+
if (host !== "127.0.0.1") {
|
|
21502
|
+
this.server = await this.bind(this.boundPort, "127.0.0.1");
|
|
21503
|
+
this.boundHost = "127.0.0.1";
|
|
21504
|
+
}
|
|
21505
|
+
throw error;
|
|
21506
|
+
}
|
|
21507
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
21508
|
+
}
|
|
21509
|
+
}
|
|
21510
|
+
/** Close every connection that did not come from this machine (sign-outs, network off). */
|
|
21511
|
+
dropRemote() {
|
|
21512
|
+
let dropped = 0;
|
|
21513
|
+
for (const socket of this.sockets) if (!isLoopbackAddress(socket.remoteAddress)) {
|
|
21514
|
+
socket.destroy();
|
|
21515
|
+
dropped++;
|
|
21516
|
+
}
|
|
21517
|
+
return dropped;
|
|
21518
|
+
}
|
|
21519
|
+
/** Stop listening and close every connection. */
|
|
21520
|
+
async close() {
|
|
21521
|
+
const server = this.server;
|
|
21522
|
+
this.server = void 0;
|
|
21523
|
+
for (const socket of this.sockets) socket.destroy();
|
|
21524
|
+
this.sockets.clear();
|
|
21525
|
+
if (server) await new Promise((resolve) => server.close(() => resolve()));
|
|
21526
|
+
}
|
|
21527
|
+
bind(port, host) {
|
|
21528
|
+
return new Promise((resolve, reject) => {
|
|
21529
|
+
const server = createServer((socket) => {
|
|
21530
|
+
this.sockets.add(socket);
|
|
21531
|
+
socket.once("close", () => this.sockets.delete(socket));
|
|
21532
|
+
this.http.emit("connection", socket);
|
|
21533
|
+
});
|
|
21534
|
+
server.once("error", reject);
|
|
21535
|
+
server.listen({
|
|
21536
|
+
port,
|
|
21537
|
+
host,
|
|
21538
|
+
exclusive: true
|
|
21539
|
+
}, () => {
|
|
21540
|
+
server.off("error", reject);
|
|
21541
|
+
resolve(server);
|
|
21542
|
+
});
|
|
21543
|
+
});
|
|
21544
|
+
}
|
|
21545
|
+
};
|
|
20911
21546
|
// ../server/src/services/errors.ts
|
|
20912
21547
|
/**
|
|
20913
21548
|
* An expected failure with a machine-readable code. Routes turn it into
|
|
@@ -20928,7 +21563,7 @@ const notFound = (message, code = "not_found") => new ServiceError(404, code, me
|
|
|
20928
21563
|
const conflict = (message, code = "conflict") => new ServiceError(409, code, message);
|
|
20929
21564
|
const forbidden = (message, code = "forbidden") => new ServiceError(403, code, message);
|
|
20930
21565
|
/** Human-readable message of any thrown value. */
|
|
20931
|
-
function errorMessage(error) {
|
|
21566
|
+
function errorMessage$1(error) {
|
|
20932
21567
|
return error instanceof Error ? error.message : String(error);
|
|
20933
21568
|
}
|
|
20934
21569
|
// ../server/src/services/runner-port.ts
|
|
@@ -21237,7 +21872,7 @@ var BrowserBridge = class {
|
|
|
21237
21872
|
send$1(v.ws, {
|
|
21238
21873
|
type: "error",
|
|
21239
21874
|
code,
|
|
21240
|
-
message: errorMessage(error)
|
|
21875
|
+
message: errorMessage$1(error)
|
|
21241
21876
|
});
|
|
21242
21877
|
}
|
|
21243
21878
|
add(v) {
|
|
@@ -21753,7 +22388,7 @@ var TerminalBridge = class {
|
|
|
21753
22388
|
}
|
|
21754
22389
|
this.handle(ptyId, msg).catch((error) => control(ws, {
|
|
21755
22390
|
type: "error",
|
|
21756
|
-
message: errorMessage(error)
|
|
22391
|
+
message: errorMessage$1(error)
|
|
21757
22392
|
}));
|
|
21758
22393
|
},
|
|
21759
22394
|
onClose: (_event, ws) => {
|
|
@@ -21827,7 +22462,7 @@ var TerminalBridge = class {
|
|
|
21827
22462
|
} catch (error) {
|
|
21828
22463
|
control(ws, {
|
|
21829
22464
|
type: "error",
|
|
21830
|
-
message: errorMessage(error)
|
|
22465
|
+
message: errorMessage$1(error)
|
|
21831
22466
|
});
|
|
21832
22467
|
ws.close(1011, "terminal unavailable");
|
|
21833
22468
|
}
|
|
@@ -21985,6 +22620,14 @@ const setCookie = (c, name, value, opt) => {
|
|
|
21985
22620
|
const cookie = generateCookie(name, value, opt);
|
|
21986
22621
|
c.header("Set-Cookie", cookie, { append: true });
|
|
21987
22622
|
};
|
|
22623
|
+
const deleteCookie = (c, name, opt) => {
|
|
22624
|
+
const deletedCookie = getCookie(c, name, opt?.prefix);
|
|
22625
|
+
setCookie(c, name, "", {
|
|
22626
|
+
...opt,
|
|
22627
|
+
maxAge: 0
|
|
22628
|
+
});
|
|
22629
|
+
return deletedCookie;
|
|
22630
|
+
};
|
|
21988
22631
|
// ../../node_modules/.pnpm/hono@4.13.17/node_modules/hono/dist/validator/validator.js
|
|
21989
22632
|
const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i;
|
|
21990
22633
|
const multipartRegex = /^multipart\/form-data(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i;
|
|
@@ -30955,7 +31598,7 @@ function unzipMarkdown(zipBase64) {
|
|
|
30955
31598
|
files = unzipSync(new Uint8Array(data), { filter: wanted });
|
|
30956
31599
|
} catch (error) {
|
|
30957
31600
|
if (error instanceof ServiceError) throw error;
|
|
30958
|
-
throw badRequest(`Not a readable zip: ${errorMessage(error)}`, "invalid_zip");
|
|
31601
|
+
throw badRequest(`Not a readable zip: ${errorMessage$1(error)}`, "invalid_zip");
|
|
30959
31602
|
}
|
|
30960
31603
|
return Object.entries(files).map(([name, bytes]) => {
|
|
30961
31604
|
if (bytes.length > AUTOMATION_IMPORT_LIMITS.maxFileChars) throw tooLarge(`${baseName(name)} is too large`);
|
|
@@ -31031,7 +31674,7 @@ function previewFile(file) {
|
|
|
31031
31674
|
fullPermissions: autonomy === "full"
|
|
31032
31675
|
};
|
|
31033
31676
|
} catch (error) {
|
|
31034
|
-
return fail(errorMessage(error));
|
|
31677
|
+
return fail(errorMessage$1(error));
|
|
31035
31678
|
}
|
|
31036
31679
|
}
|
|
31037
31680
|
/** Preview every file of an import. */
|
|
@@ -31304,6 +31947,271 @@ function imageRoutes(s) {
|
|
|
31304
31947
|
function inboxRoutes(s) {
|
|
31305
31948
|
return new Hono().get("/", queryOf(ListInboxQuery), (c) => c.json(s.inbox.list(c.req.valid("query")))).get("/counts", (c) => c.json(s.inbox.counts())).get("/:id", (c) => c.json(s.inbox.get(c.req.param("id")))).post("/:id/resolve", jsonBody(ResolveInboxRequest), async (c) => c.json(await s.inbox.resolve(c.req.param("id"), c.req.valid("json"))));
|
|
31306
31949
|
}
|
|
31950
|
+
// ../server/src/token.ts
|
|
31951
|
+
/** Generate a random URL-safe token (hex). */
|
|
31952
|
+
function generateToken(bytes = 32) {
|
|
31953
|
+
return randomBytes(bytes).toString("hex");
|
|
31954
|
+
}
|
|
31955
|
+
/**
|
|
31956
|
+
* Load the per-install token from `file`, creating it on first run (§15). The file is
|
|
31957
|
+
* readable only by the user where the OS supports POSIX modes.
|
|
31958
|
+
*/
|
|
31959
|
+
function ensureInstallToken(file) {
|
|
31960
|
+
if (existsSync(file)) {
|
|
31961
|
+
const existing = readFileSync(file, "utf8").trim();
|
|
31962
|
+
if (existing.length >= 32) return existing;
|
|
31963
|
+
}
|
|
31964
|
+
const token = generateToken();
|
|
31965
|
+
writeFileSync(file, `${token}\n`, { mode: 384 });
|
|
31966
|
+
return token;
|
|
31967
|
+
}
|
|
31968
|
+
/** Constant-time string comparison. */
|
|
31969
|
+
function tokensEqual(a, b) {
|
|
31970
|
+
if (!a) return false;
|
|
31971
|
+
const left = Buffer.from(a);
|
|
31972
|
+
const right = Buffer.from(b);
|
|
31973
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
31974
|
+
}
|
|
31975
|
+
// ../server/src/security.ts
|
|
31976
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
31977
|
+
"127.0.0.1",
|
|
31978
|
+
"localhost",
|
|
31979
|
+
"[::1]",
|
|
31980
|
+
"::1"
|
|
31981
|
+
]);
|
|
31982
|
+
/** Files a browser fetches without cookies to install the app (manifest, icons). */
|
|
31983
|
+
const PWA_FILES = /* @__PURE__ */ new Set([
|
|
31984
|
+
"/manifest.webmanifest",
|
|
31985
|
+
"/favicon.svg",
|
|
31986
|
+
"/apple-touch-icon.png",
|
|
31987
|
+
"/icons/icon-192.png",
|
|
31988
|
+
"/icons/icon-512.png",
|
|
31989
|
+
"/icons/maskable-192.png",
|
|
31990
|
+
"/icons/maskable-512.png"
|
|
31991
|
+
]);
|
|
31992
|
+
/** Sign-in endpoints a signed-out remote device may call. */
|
|
31993
|
+
const AUTH_PATHS = /* @__PURE__ */ new Set([
|
|
31994
|
+
"/api/auth/status",
|
|
31995
|
+
"/api/auth/sign-in",
|
|
31996
|
+
"/api/auth/sign-out"
|
|
31997
|
+
]);
|
|
31998
|
+
/** Hostname part of a Host header (`127.0.0.1:4317` -> `127.0.0.1`, `[::1]:80` -> `[::1]`). */
|
|
31999
|
+
function hostnameOf(host) {
|
|
32000
|
+
if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
|
|
32001
|
+
const colon = host.lastIndexOf(":");
|
|
32002
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
32003
|
+
}
|
|
32004
|
+
/** Origins for a server listening on `port`. */
|
|
32005
|
+
function loopbackOrigins(port) {
|
|
32006
|
+
return [
|
|
32007
|
+
`http://127.0.0.1:${port}`,
|
|
32008
|
+
`http://localhost:${port}`,
|
|
32009
|
+
`http://[::1]:${port}`
|
|
32010
|
+
];
|
|
32011
|
+
}
|
|
32012
|
+
/**
|
|
32013
|
+
* The connection's peer address, from the socket itself. Headers such as `X-Forwarded-For`
|
|
32014
|
+
* are never consulted, so they can't make a request look local. Unknown counts as remote.
|
|
32015
|
+
*/
|
|
32016
|
+
function peerAddress(c) {
|
|
32017
|
+
const env = c.env;
|
|
32018
|
+
return normalizeAddress(env?.incoming?.socket?.remoteAddress);
|
|
32019
|
+
}
|
|
32020
|
+
/** Did this request come from another device? */
|
|
32021
|
+
function isRemoteRequest(c) {
|
|
32022
|
+
return !isLoopbackAddress(peerAddress(c));
|
|
32023
|
+
}
|
|
32024
|
+
/** `Host` values for the machine's own addresses on `port`. */
|
|
32025
|
+
function ownHostValues(remote) {
|
|
32026
|
+
const port = remote.port();
|
|
32027
|
+
const values = /* @__PURE__ */ new Set();
|
|
32028
|
+
for (const host of remote.ownHosts()) {
|
|
32029
|
+
values.add(`${host}:${port}`);
|
|
32030
|
+
if (port === 80) values.add(host);
|
|
32031
|
+
}
|
|
32032
|
+
return values;
|
|
32033
|
+
}
|
|
32034
|
+
/** Static UI files (served to signed-out remote devices so they can show the sign-in page). */
|
|
32035
|
+
function isShellRequest(c) {
|
|
32036
|
+
const path = c.req.path;
|
|
32037
|
+
return (c.req.method === "GET" || c.req.method === "HEAD") && !path.startsWith("/api/") && path !== "/ws" && !path.startsWith("/ws/") && path !== "/mcp";
|
|
32038
|
+
}
|
|
32039
|
+
/**
|
|
32040
|
+
* Request checks (§15, §15.1):
|
|
32041
|
+
* 1. `Host` must be a loopback name, or, with network access on, one of this machine's own
|
|
32042
|
+
* LAN/VPN addresses on the server port (blocks DNS rebinding).
|
|
32043
|
+
* 2. `Origin`, when present, must be one of ours (blocks other websites, including other
|
|
32044
|
+
* ports on the same host, which count as same-site for cookies, and `null`).
|
|
32045
|
+
* 3. Loopback peers need the install token via `Authorization: Bearer`, the HttpOnly cookie,
|
|
32046
|
+
* or `?token=` (exchanged for the cookie and stripped from the URL with a redirect).
|
|
32047
|
+
* 4. Remote peers need a remote session cookie (from the password sign-in); the install token
|
|
32048
|
+
* is never accepted from them, and loopback-only paths answer 404.
|
|
32049
|
+
*/
|
|
32050
|
+
function securityMiddleware(opts) {
|
|
32051
|
+
return async (c, next) => {
|
|
32052
|
+
const remote = isRemoteRequest(c);
|
|
32053
|
+
const networkOn = opts.remote?.enabled() ?? false;
|
|
32054
|
+
if (remote && !networkOn) return c.text("Network access is off", 403);
|
|
32055
|
+
const host = c.req.header("host");
|
|
32056
|
+
const hostName = host ? hostnameOf(host).toLowerCase() : "";
|
|
32057
|
+
if (!(Boolean(host) && (LOOPBACK_HOSTS.has(hostName) || networkOn && opts.remote !== void 0 && ownHostValues(opts.remote).has(host)))) return c.text("Forbidden host", 403);
|
|
32058
|
+
const origin = c.req.header("origin");
|
|
32059
|
+
if (origin && !opts.allowedOrigins().has(origin)) return c.text("Forbidden origin", 403);
|
|
32060
|
+
if (remote) return remoteGate(c, next, opts);
|
|
32061
|
+
if (opts.publicPaths?.has(c.req.path) || PWA_FILES.has(c.req.path)) return next();
|
|
32062
|
+
if (AUTH_PATHS.has(c.req.path)) return next();
|
|
32063
|
+
const queryToken = c.req.query("token");
|
|
32064
|
+
if (queryToken !== void 0) {
|
|
32065
|
+
if (!tokensEqual(queryToken, opts.token)) return c.text("Invalid token", 401);
|
|
32066
|
+
setCookie(c, opts.cookieName(), opts.token, {
|
|
32067
|
+
httpOnly: true,
|
|
32068
|
+
sameSite: "Strict",
|
|
32069
|
+
path: "/"
|
|
32070
|
+
});
|
|
32071
|
+
if (c.req.method === "GET" && !c.req.path.startsWith("/api/") && c.req.path !== "/ws") {
|
|
32072
|
+
const url = new URL(c.req.url);
|
|
32073
|
+
url.searchParams.delete("token");
|
|
32074
|
+
return c.redirect(`${url.pathname}${url.search}${url.hash}`, 302);
|
|
32075
|
+
}
|
|
32076
|
+
return next();
|
|
32077
|
+
}
|
|
32078
|
+
const auth = c.req.header("authorization");
|
|
32079
|
+
if (tokensEqual(auth?.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : void 0, opts.token) || tokensEqual(getCookie(c, opts.cookieName()), opts.token)) return next();
|
|
32080
|
+
if (c.req.header("accept")?.includes("text/html")) return c.html(UNAUTHORIZED_PAGE, 401);
|
|
32081
|
+
return c.json({ error: {
|
|
32082
|
+
code: "unauthorized",
|
|
32083
|
+
message: "Missing or invalid token"
|
|
32084
|
+
} }, 401);
|
|
32085
|
+
};
|
|
32086
|
+
}
|
|
32087
|
+
/** Remote peers: loopback-only paths are hidden, everything else needs a session. */
|
|
32088
|
+
async function remoteGate(c, next, opts) {
|
|
32089
|
+
const path = c.req.path;
|
|
32090
|
+
if (opts.loopbackOnlyPaths?.has(path)) return c.text("Not found", 404);
|
|
32091
|
+
if (path === "/api/health" || AUTH_PATHS.has(path) || PWA_FILES.has(path)) {
|
|
32092
|
+
await next();
|
|
32093
|
+
return;
|
|
32094
|
+
}
|
|
32095
|
+
if (opts.remote?.verifySession(getCookie(c, opts.remote.sessionCookieName()))) {
|
|
32096
|
+
await next();
|
|
32097
|
+
return;
|
|
32098
|
+
}
|
|
32099
|
+
if (isShellRequest(c)) {
|
|
32100
|
+
await next();
|
|
32101
|
+
return;
|
|
32102
|
+
}
|
|
32103
|
+
return c.json({ error: {
|
|
32104
|
+
code: "sign_in_required",
|
|
32105
|
+
message: "Sign in to CodeDen"
|
|
32106
|
+
} }, 401);
|
|
32107
|
+
}
|
|
32108
|
+
const UNAUTHORIZED_PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>CodeDen</title>
|
|
32109
|
+
<style>body{font-family:system-ui,sans-serif;background:#0b0d10;color:#e6e8eb;display:grid;place-items:center;height:100vh;margin:0}
|
|
32110
|
+
main{max-width:28rem;text-align:center}code{background:#1b1f24;padding:.15rem .4rem;border-radius:.3rem}</style></head>
|
|
32111
|
+
<body><main><h1>CodeDen is locked</h1><p>Open CodeDen with <code>npx codeden</code> to get an authenticated link.</p></main></body></html>`;
|
|
32112
|
+
// ../server/src/routes/network.ts
|
|
32113
|
+
/** Session id of a remote caller, from its cookie. */
|
|
32114
|
+
function remoteSessionId(c, ctx) {
|
|
32115
|
+
return ctx.network.store.verifySession(getCookie(c, ctx.sessionCookie()))?.id ?? null;
|
|
32116
|
+
}
|
|
32117
|
+
/** Changing network access is for this machine only (and the CLI). */
|
|
32118
|
+
function requireLocal(c) {
|
|
32119
|
+
if (isRemoteRequest(c)) throw forbidden("Network access can only be changed on the computer running CodeDen.");
|
|
32120
|
+
}
|
|
32121
|
+
/**
|
|
32122
|
+
* `/api/network` (§15.1): status for everyone signed in; turning access on or off and the
|
|
32123
|
+
* password are loopback-only. Signed-in devices can be signed out from anywhere.
|
|
32124
|
+
*/
|
|
32125
|
+
function networkRoutes(ctx) {
|
|
32126
|
+
const { network } = ctx;
|
|
32127
|
+
const statusFor = (c) => network.status({
|
|
32128
|
+
remote: isRemoteRequest(c),
|
|
32129
|
+
sessionId: remoteSessionId(c, ctx)
|
|
32130
|
+
});
|
|
32131
|
+
return new Hono().get("/", (c) => c.json(statusFor(c))).put("/", jsonBody(SetNetworkAccessRequest), async (c) => {
|
|
32132
|
+
requireLocal(c);
|
|
32133
|
+
const { enabled } = c.req.valid("json");
|
|
32134
|
+
if (enabled && !network.store.passwordSet) throw badRequest("Set a password before turning network access on.", "password_required");
|
|
32135
|
+
await network.setEnabled(enabled);
|
|
32136
|
+
return c.json(statusFor(c));
|
|
32137
|
+
}).post("/password", jsonBody(SetNetworkPasswordRequest), async (c) => {
|
|
32138
|
+
requireLocal(c);
|
|
32139
|
+
let generated;
|
|
32140
|
+
try {
|
|
32141
|
+
generated = await network.setPassword(c.req.valid("json"));
|
|
32142
|
+
} catch (error) {
|
|
32143
|
+
if (error instanceof PasswordRuleError) throw badRequest(error.message, error.code);
|
|
32144
|
+
throw error;
|
|
32145
|
+
}
|
|
32146
|
+
const body = {
|
|
32147
|
+
status: statusFor(c),
|
|
32148
|
+
generated
|
|
32149
|
+
};
|
|
32150
|
+
return c.json(body);
|
|
32151
|
+
}).delete("/devices/:id", (c) => {
|
|
32152
|
+
if (!network.revoke(c.req.param("id"))) throw notFound("No such device");
|
|
32153
|
+
return c.json(statusFor(c));
|
|
32154
|
+
}).delete("/devices", (c) => {
|
|
32155
|
+
network.revokeAll();
|
|
32156
|
+
return c.json(statusFor(c));
|
|
32157
|
+
});
|
|
32158
|
+
}
|
|
32159
|
+
/** `/api/auth` (§15.1): sign-in for remote devices. Public; Host/Origin checks still apply. */
|
|
32160
|
+
function authRoutes(ctx) {
|
|
32161
|
+
const { network } = ctx;
|
|
32162
|
+
return new Hono().get("/status", (c) => {
|
|
32163
|
+
const remote = isRemoteRequest(c);
|
|
32164
|
+
const body = {
|
|
32165
|
+
remote,
|
|
32166
|
+
signedIn: remote ? remoteSessionId(c, ctx) !== null : tokensEqual(getCookie(c, ctx.tokenCookie()), ctx.token),
|
|
32167
|
+
networkEnabled: network.enabled
|
|
32168
|
+
};
|
|
32169
|
+
return c.json(body);
|
|
32170
|
+
}).post("/sign-in", jsonBody(SignInRequest), async (c) => {
|
|
32171
|
+
const address = peerAddress(c);
|
|
32172
|
+
const refuse = (status, code, message, retryAfterMs = null) => {
|
|
32173
|
+
const retryAfterSec = retryAfterMs === null ? null : Math.ceil(retryAfterMs / 1e3);
|
|
32174
|
+
if (retryAfterSec !== null) c.header("Retry-After", String(retryAfterSec));
|
|
32175
|
+
const body = { error: {
|
|
32176
|
+
code,
|
|
32177
|
+
message,
|
|
32178
|
+
retryAfterSec
|
|
32179
|
+
} };
|
|
32180
|
+
return c.json(body, status);
|
|
32181
|
+
};
|
|
32182
|
+
if (!network.enabled) return refuse(403, "network_off", "Network access is off.");
|
|
32183
|
+
if (!isRemoteRequest(c)) return refuse(403, "network_off", "Sign-in is for other devices; this computer uses its own link.");
|
|
32184
|
+
const gate = network.limiter.check(address);
|
|
32185
|
+
if (!gate.allowed) {
|
|
32186
|
+
const seconds = Math.ceil(gate.retryAfterMs / 1e3);
|
|
32187
|
+
return gate.reason === "locked_out" ? refuse(429, "locked_out", `Too many wrong passwords. Try again in ${Math.ceil(seconds / 60)} min.`, gate.retryAfterMs) : refuse(429, "rate_limited", `Wait ${seconds} s before trying again.`, gate.retryAfterMs);
|
|
32188
|
+
}
|
|
32189
|
+
const { password } = c.req.valid("json");
|
|
32190
|
+
if (!await network.store.checkPassword(password)) {
|
|
32191
|
+
network.limiter.fail(address);
|
|
32192
|
+
network.recordFailedSignIn(address, "wrong password");
|
|
32193
|
+
return refuse(401, "invalid_password", "That password is not right.");
|
|
32194
|
+
}
|
|
32195
|
+
network.limiter.succeed(address);
|
|
32196
|
+
const { token, session } = network.store.createSession({
|
|
32197
|
+
label: c.req.header("user-agent") ?? "",
|
|
32198
|
+
address: address || null
|
|
32199
|
+
});
|
|
32200
|
+
setCookie(c, ctx.sessionCookie(), token, {
|
|
32201
|
+
httpOnly: true,
|
|
32202
|
+
sameSite: "Lax",
|
|
32203
|
+
secure: new URL(c.req.url).protocol === "https:",
|
|
32204
|
+
path: "/",
|
|
32205
|
+
maxAge: Math.floor((Date.parse(session.expiresAt) - Date.now()) / 1e3)
|
|
32206
|
+
});
|
|
32207
|
+
return c.json({ ok: true });
|
|
32208
|
+
}).post("/sign-out", (c) => {
|
|
32209
|
+
const id = remoteSessionId(c, ctx);
|
|
32210
|
+
if (id) network.store.revoke(id);
|
|
32211
|
+
deleteCookie(c, ctx.sessionCookie(), { path: "/" });
|
|
32212
|
+
return c.json({ ok: true });
|
|
32213
|
+
});
|
|
32214
|
+
}
|
|
31307
32215
|
// ../server/src/routes/orchestration.ts
|
|
31308
32216
|
/**
|
|
31309
32217
|
* `/api/orchestration`: the orchestrator and PM chats (§8.1), notices, digests.
|
|
@@ -32258,7 +33166,7 @@ function createApi(ctx) {
|
|
|
32258
33166
|
uptimeSec: (Date.now() - ctx.startedAt) / 1e3
|
|
32259
33167
|
};
|
|
32260
33168
|
return c.json(body);
|
|
32261
|
-
}).get("/snapshot", (c) => c.json(buildSnapshot(ctx))).get("/activity", (c) => c.json(buildActivity(s))).get("/sessions/:id/snapshot", (c) => c.json(buildSessionSnapshot(s, c.req.param("id")))).route("/projects", projectRoutes(s)).route("/projects", projectGithubRoutes(s)).route("/projects", projectFactoryRoutes(s)).route("/factory", factoryRoutes(s)).route("/prs", prRoutes(s)).route("/github", githubRoutes(s)).route("/sessions", sessionGithubRoutes(s)).route("/sessions", sessionRoutes(s)).route("/sessions", workbenchRoutes(s)).route("/sessions", sessionBrowserRoutes(s)).route("/browser", browserRoutes(s)).route("/images", imageRoutes(s)).route("/agent-config", agentConfigRoutes(s)).route("/voice", voiceRoutes(s)).route("/", speechRoutes(s)).route("/sessions", sessionAgentConfigRoutes(s)).route("/sessions", sessionFileRoutes(s)).route("/utility", utilityRoutes(s)).route("/inbox", inboxRoutes(s)).route("/orchestration", orchestrationRoutes(s)).route("/conflicts", conflictRoutes(s)).route("/automations", automationRoutes(s)).route("/projects", projectAutomationRoutes(s)).route("/usage", usageRoutes(s)).route("/setup", setupRoutes(s)).route("/updates", updateRoutes(s)).route("/artifacts", artifactRoutes(s)).route("/secrets", secretRoutes(s)).route("/", fileRoutes(s)).get("/settings", (c) => c.json(s.settings.get())).patch("/settings", async (c) => {
|
|
33169
|
+
}).get("/snapshot", (c) => c.json(buildSnapshot(ctx))).get("/activity", (c) => c.json(buildActivity(s))).get("/sessions/:id/snapshot", (c) => c.json(buildSessionSnapshot(s, c.req.param("id")))).route("/projects", projectRoutes(s)).route("/projects", projectGithubRoutes(s)).route("/projects", projectFactoryRoutes(s)).route("/factory", factoryRoutes(s)).route("/prs", prRoutes(s)).route("/github", githubRoutes(s)).route("/sessions", sessionGithubRoutes(s)).route("/sessions", sessionRoutes(s)).route("/sessions", workbenchRoutes(s)).route("/sessions", sessionBrowserRoutes(s)).route("/browser", browserRoutes(s)).route("/images", imageRoutes(s)).route("/agent-config", agentConfigRoutes(s)).route("/voice", voiceRoutes(s)).route("/", speechRoutes(s)).route("/sessions", sessionAgentConfigRoutes(s)).route("/sessions", sessionFileRoutes(s)).route("/utility", utilityRoutes(s)).route("/inbox", inboxRoutes(s)).route("/orchestration", orchestrationRoutes(s)).route("/conflicts", conflictRoutes(s)).route("/automations", automationRoutes(s)).route("/projects", projectAutomationRoutes(s)).route("/usage", usageRoutes(s)).route("/setup", setupRoutes(s)).route("/updates", updateRoutes(s)).route("/artifacts", artifactRoutes(s)).route("/secrets", secretRoutes(s)).route("/network", networkRoutes(ctx.network)).route("/auth", authRoutes(ctx.network)).route("/", fileRoutes(s)).get("/settings", (c) => c.json(s.settings.get())).patch("/settings", async (c) => {
|
|
32262
33170
|
const patch = await c.req.json().catch(() => void 0);
|
|
32263
33171
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw badRequest("Expected a JSON object");
|
|
32264
33172
|
let settings;
|
|
@@ -32637,95 +33545,6 @@ function indexReader(file) {
|
|
|
32637
33545
|
}
|
|
32638
33546
|
};
|
|
32639
33547
|
}
|
|
32640
|
-
// ../server/src/token.ts
|
|
32641
|
-
/** Generate a random URL-safe token (hex). */
|
|
32642
|
-
function generateToken(bytes = 32) {
|
|
32643
|
-
return randomBytes(bytes).toString("hex");
|
|
32644
|
-
}
|
|
32645
|
-
/**
|
|
32646
|
-
* Load the per-install token from `file`, creating it on first run (§15). The file is
|
|
32647
|
-
* readable only by the user where the OS supports POSIX modes.
|
|
32648
|
-
*/
|
|
32649
|
-
function ensureInstallToken(file) {
|
|
32650
|
-
if (existsSync(file)) {
|
|
32651
|
-
const existing = readFileSync(file, "utf8").trim();
|
|
32652
|
-
if (existing.length >= 32) return existing;
|
|
32653
|
-
}
|
|
32654
|
-
const token = generateToken();
|
|
32655
|
-
writeFileSync(file, `${token}\n`, { mode: 384 });
|
|
32656
|
-
return token;
|
|
32657
|
-
}
|
|
32658
|
-
/** Constant-time string comparison. */
|
|
32659
|
-
function tokensEqual(a, b) {
|
|
32660
|
-
if (!a) return false;
|
|
32661
|
-
const left = Buffer.from(a);
|
|
32662
|
-
const right = Buffer.from(b);
|
|
32663
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
32664
|
-
}
|
|
32665
|
-
// ../server/src/security.ts
|
|
32666
|
-
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
32667
|
-
"127.0.0.1",
|
|
32668
|
-
"localhost",
|
|
32669
|
-
"[::1]",
|
|
32670
|
-
"::1"
|
|
32671
|
-
]);
|
|
32672
|
-
/** Hostname part of a Host header (`127.0.0.1:4317` -> `127.0.0.1`, `[::1]:80` -> `[::1]`). */
|
|
32673
|
-
function hostnameOf(host) {
|
|
32674
|
-
if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
|
|
32675
|
-
const colon = host.lastIndexOf(":");
|
|
32676
|
-
return colon === -1 ? host : host.slice(0, colon);
|
|
32677
|
-
}
|
|
32678
|
-
/** Origins for a server listening on `port`. */
|
|
32679
|
-
function loopbackOrigins(port) {
|
|
32680
|
-
return [
|
|
32681
|
-
`http://127.0.0.1:${port}`,
|
|
32682
|
-
`http://localhost:${port}`,
|
|
32683
|
-
`http://[::1]:${port}`
|
|
32684
|
-
];
|
|
32685
|
-
}
|
|
32686
|
-
/**
|
|
32687
|
-
* Request checks (§15):
|
|
32688
|
-
* 1. `Host` must be a loopback name (blocks DNS rebinding).
|
|
32689
|
-
* 2. `Origin`, when present, must be one of ours (blocks other websites, including other
|
|
32690
|
-
* localhost ports, which count as same-site for cookies).
|
|
32691
|
-
* 3. A valid token via `Authorization: Bearer`, the HttpOnly cookie, or `?token=` (which is
|
|
32692
|
-
* exchanged for the cookie and stripped from the URL with a redirect).
|
|
32693
|
-
*/
|
|
32694
|
-
function securityMiddleware(opts) {
|
|
32695
|
-
return async (c, next) => {
|
|
32696
|
-
const host = c.req.header("host");
|
|
32697
|
-
if (!host || !LOOPBACK_HOSTS.has(hostnameOf(host).toLowerCase())) return c.text("Forbidden host", 403);
|
|
32698
|
-
const origin = c.req.header("origin");
|
|
32699
|
-
if (origin && !opts.allowedOrigins().has(origin)) return c.text("Forbidden origin", 403);
|
|
32700
|
-
if (opts.publicPaths?.has(c.req.path)) return next();
|
|
32701
|
-
const queryToken = c.req.query("token");
|
|
32702
|
-
if (queryToken !== void 0) {
|
|
32703
|
-
if (!tokensEqual(queryToken, opts.token)) return c.text("Invalid token", 401);
|
|
32704
|
-
setCookie(c, opts.cookieName(), opts.token, {
|
|
32705
|
-
httpOnly: true,
|
|
32706
|
-
sameSite: "Strict",
|
|
32707
|
-
path: "/"
|
|
32708
|
-
});
|
|
32709
|
-
if (c.req.method === "GET" && !c.req.path.startsWith("/api/") && c.req.path !== "/ws") {
|
|
32710
|
-
const url = new URL(c.req.url);
|
|
32711
|
-
url.searchParams.delete("token");
|
|
32712
|
-
return c.redirect(`${url.pathname}${url.search}${url.hash}`, 302);
|
|
32713
|
-
}
|
|
32714
|
-
return next();
|
|
32715
|
-
}
|
|
32716
|
-
const auth = c.req.header("authorization");
|
|
32717
|
-
if (tokensEqual(auth?.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : void 0, opts.token) || tokensEqual(getCookie(c, opts.cookieName()), opts.token)) return next();
|
|
32718
|
-
if (c.req.header("accept")?.includes("text/html")) return c.html(UNAUTHORIZED_PAGE, 401);
|
|
32719
|
-
return c.json({ error: {
|
|
32720
|
-
code: "unauthorized",
|
|
32721
|
-
message: "Missing or invalid token"
|
|
32722
|
-
} }, 401);
|
|
32723
|
-
};
|
|
32724
|
-
}
|
|
32725
|
-
const UNAUTHORIZED_PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>CodeDen</title>
|
|
32726
|
-
<style>body{font-family:system-ui,sans-serif;background:#0b0d10;color:#e6e8eb;display:grid;place-items:center;height:100vh;margin:0}
|
|
32727
|
-
main{max-width:28rem;text-align:center}code{background:#1b1f24;padding:.15rem .4rem;border-radius:.3rem}</style></head>
|
|
32728
|
-
<body><main><h1>CodeDen is locked</h1><p>Open CodeDen with <code>npx codeden</code> to get an authenticated link.</p></main></body></html>`;
|
|
32729
33548
|
// ../server/src/mcp/define.ts
|
|
32730
33549
|
/** Identity helper that infers a tool's input and scope types. */
|
|
32731
33550
|
function defineTool(tool) {
|
|
@@ -32816,7 +33635,7 @@ var ToolRegistry = class {
|
|
|
32816
33635
|
};
|
|
32817
33636
|
this.log?.(`tool ${name} failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
32818
33637
|
return {
|
|
32819
|
-
text: `${name} failed: ${errorMessage(error)}`,
|
|
33638
|
+
text: `${name} failed: ${errorMessage$1(error)}`,
|
|
32820
33639
|
isError: true
|
|
32821
33640
|
};
|
|
32822
33641
|
}
|
|
@@ -43322,7 +44141,7 @@ var AutomationSources = class {
|
|
|
43322
44141
|
});
|
|
43323
44142
|
this.watchers.set(dir, watcher);
|
|
43324
44143
|
} catch (error) {
|
|
43325
|
-
this.deps.log?.(`automations: cannot watch ${dir}: ${errorMessage(error)}`);
|
|
44144
|
+
this.deps.log?.(`automations: cannot watch ${dir}: ${errorMessage$1(error)}`);
|
|
43326
44145
|
}
|
|
43327
44146
|
}
|
|
43328
44147
|
}
|
|
@@ -43333,7 +44152,7 @@ var AutomationSources = class {
|
|
|
43333
44152
|
try {
|
|
43334
44153
|
this.reload();
|
|
43335
44154
|
} catch (error) {
|
|
43336
|
-
this.deps.log?.(`automations: reload failed: ${errorMessage(error)}`);
|
|
44155
|
+
this.deps.log?.(`automations: reload failed: ${errorMessage$1(error)}`);
|
|
43337
44156
|
}
|
|
43338
44157
|
}, 200);
|
|
43339
44158
|
this.timer.unref();
|
|
@@ -44035,14 +44854,14 @@ var AutomationExecutor = class {
|
|
|
44035
44854
|
}
|
|
44036
44855
|
}
|
|
44037
44856
|
} catch (error) {
|
|
44038
|
-
this.deps.log?.(`automations: run of ${automation.id} failed: ${errorMessage(error)}`);
|
|
44857
|
+
this.deps.log?.(`automations: run of ${automation.id} failed: ${errorMessage$1(error)}`);
|
|
44039
44858
|
return {
|
|
44040
44859
|
status: "started",
|
|
44041
44860
|
run: this.deps.runs.save({
|
|
44042
44861
|
...run,
|
|
44043
44862
|
status: "failed",
|
|
44044
44863
|
finishedAt: this.deps.now().toISOString(),
|
|
44045
|
-
error: { message: errorMessage(error) }
|
|
44864
|
+
error: { message: errorMessage$1(error) }
|
|
44046
44865
|
}),
|
|
44047
44866
|
sessionId: null
|
|
44048
44867
|
};
|
|
@@ -44397,7 +45216,7 @@ var CronScheduler = class {
|
|
|
44397
45216
|
else if (pair.automation.guards.catchUpMissed) await this.deps.fire(pair, "catch_up");
|
|
44398
45217
|
else this.deps.log?.(`automations: missed ${pair.automation.id} at ${at} (no catch-up)`);
|
|
44399
45218
|
} catch (error) {
|
|
44400
|
-
this.deps.log?.(`automations: ${pair.automation.id} failed to fire: ${errorMessage(error)}`);
|
|
45219
|
+
this.deps.log?.(`automations: ${pair.automation.id} failed to fire: ${errorMessage$1(error)}`);
|
|
44401
45220
|
}
|
|
44402
45221
|
}
|
|
44403
45222
|
} finally {
|
|
@@ -44922,7 +45741,7 @@ var AutomationService = class {
|
|
|
44922
45741
|
this.syncSessions(events);
|
|
44923
45742
|
this.dispatch(events);
|
|
44924
45743
|
} catch (error) {
|
|
44925
|
-
this.deps.log?.(`automations: ${errorMessage(error)}`);
|
|
45744
|
+
this.deps.log?.(`automations: ${errorMessage$1(error)}`);
|
|
44926
45745
|
}
|
|
44927
45746
|
}
|
|
44928
45747
|
syncSessions(events) {
|
|
@@ -44963,7 +45782,7 @@ var AutomationService = class {
|
|
|
44963
45782
|
this.fire({
|
|
44964
45783
|
...req,
|
|
44965
45784
|
issue: event.issue
|
|
44966
|
-
}).catch((error) => this.deps.log?.(`automations: event run of ${automation.id}: ${errorMessage(error)}`));
|
|
45785
|
+
}).catch((error) => this.deps.log?.(`automations: event run of ${automation.id}: ${errorMessage$1(error)}`));
|
|
44967
45786
|
}
|
|
44968
45787
|
}
|
|
44969
45788
|
}
|
|
@@ -45565,7 +46384,7 @@ async function runAudit(ctx, command, cwd) {
|
|
|
45565
46384
|
} catch (error) {
|
|
45566
46385
|
return {
|
|
45567
46386
|
ok: false,
|
|
45568
|
-
detail: errorMessage(error)
|
|
46387
|
+
detail: errorMessage$1(error)
|
|
45569
46388
|
};
|
|
45570
46389
|
}
|
|
45571
46390
|
}
|
|
@@ -45746,7 +46565,7 @@ async function advanceWatching(ctx, project, loop, item) {
|
|
|
45746
46565
|
} catch (error) {
|
|
45747
46566
|
ctx.store.updateItem(item.id, (i) => ({
|
|
45748
46567
|
...i,
|
|
45749
|
-
note: `Could not read main CI: ${errorMessage(error)}`
|
|
46568
|
+
note: `Could not read main CI: ${errorMessage$1(error)}`
|
|
45750
46569
|
}));
|
|
45751
46570
|
return loop;
|
|
45752
46571
|
}
|
|
@@ -45780,7 +46599,7 @@ async function regression(ctx, project, loop, item) {
|
|
|
45780
46599
|
prNumber,
|
|
45781
46600
|
title: item.title
|
|
45782
46601
|
}).catch((error) => {
|
|
45783
|
-
ctx.log?.(`factory: revert via API: ${errorMessage(error)}`);
|
|
46602
|
+
ctx.log?.(`factory: revert via API: ${errorMessage$1(error)}`);
|
|
45784
46603
|
return null;
|
|
45785
46604
|
});
|
|
45786
46605
|
const base = {
|
|
@@ -46105,7 +46924,7 @@ async function advanceVerifying(ctx, project, loop, item) {
|
|
|
46105
46924
|
} catch (error) {
|
|
46106
46925
|
ctx.store.updateItem(item.id, (i) => ({
|
|
46107
46926
|
...i,
|
|
46108
|
-
note: `Could not read the PR: ${errorMessage(error)}`
|
|
46927
|
+
note: `Could not read the PR: ${errorMessage$1(error)}`
|
|
46109
46928
|
}));
|
|
46110
46929
|
return loop;
|
|
46111
46930
|
}
|
|
@@ -46182,7 +47001,7 @@ async function advanceVerifying(ctx, project, loop, item) {
|
|
|
46182
47001
|
}).catch((error) => ({
|
|
46183
47002
|
merged: false,
|
|
46184
47003
|
sha: null,
|
|
46185
|
-
error: errorMessage(error)
|
|
47004
|
+
error: errorMessage$1(error)
|
|
46186
47005
|
}));
|
|
46187
47006
|
if (!res.merged) {
|
|
46188
47007
|
const why = "error" in res ? res.error : "GitHub did not merge it";
|
|
@@ -46206,7 +47025,7 @@ async function advanceVerifying(ctx, project, loop, item) {
|
|
|
46206
47025
|
}));
|
|
46207
47026
|
if (current.issue && current.source !== "revert") {
|
|
46208
47027
|
const passed = checks.filter((c) => c.state === "pass").map((c) => c.name).join(", ");
|
|
46209
|
-
await ctx.github.comment(project.id, current.issue.number, `Merged automatically by the CodeDen factory in #${pr.ref.number} after every merge check passed (${passed}).`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage(error)}`));
|
|
47028
|
+
await ctx.github.comment(project.id, current.issue.number, `Merged automatically by the CodeDen factory in #${pr.ref.number} after every merge check passed (${passed}).`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage$1(error)}`));
|
|
46210
47029
|
}
|
|
46211
47030
|
factoryNotice(ctx, {
|
|
46212
47031
|
id: `factory.merged:${current.id}`,
|
|
@@ -46335,7 +47154,7 @@ async function handleFailure(ctx, project, loop, item) {
|
|
|
46335
47154
|
return loop;
|
|
46336
47155
|
}
|
|
46337
47156
|
const next = failItem(ctx, loop, item, failed.map((c) => `${c.name}: ${c.detail ?? "failed"}`).join("; ") || "Merge checks failed");
|
|
46338
|
-
if (item.pr) await ctx.github.comment(project.id, item.pr.number, `The CodeDen factory's merge checks failed, so this PR is left for a human:\n\n${failed.map((c) => `- **${c.name}**: ${c.detail ?? "failed"}`).join("\n")}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage(error)}`));
|
|
47157
|
+
if (item.pr) await ctx.github.comment(project.id, item.pr.number, `The CodeDen factory's merge checks failed, so this PR is left for a human:\n\n${failed.map((c) => `- **${c.name}**: ${c.detail ?? "failed"}`).join("\n")}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage$1(error)}`));
|
|
46339
47158
|
return next;
|
|
46340
47159
|
}
|
|
46341
47160
|
// ../server/src/services/factory/loop.ts
|
|
@@ -46398,10 +47217,10 @@ async function runTick(ctx, projectId, hooks) {
|
|
|
46398
47217
|
else if (stage === "verifying") loop = await advanceVerifying(ctx, project, loop, item);
|
|
46399
47218
|
else loop = await advanceWatching(ctx, project, loop, item);
|
|
46400
47219
|
} catch (error) {
|
|
46401
|
-
ctx.log?.(`factory: ${item.id}: ${errorMessage(error)}`);
|
|
47220
|
+
ctx.log?.(`factory: ${item.id}: ${errorMessage$1(error)}`);
|
|
46402
47221
|
ctx.store.updateItem(item.id, (i) => ({
|
|
46403
47222
|
...i,
|
|
46404
|
-
note: `Loop error: ${errorMessage(error)}`
|
|
47223
|
+
note: `Loop error: ${errorMessage$1(error)}`
|
|
46405
47224
|
}));
|
|
46406
47225
|
}
|
|
46407
47226
|
if (loop.state !== "paused") {
|
|
@@ -46528,7 +47347,7 @@ async function discover(ctx, project, loop, now) {
|
|
|
46528
47347
|
startedAt: now.toISOString()
|
|
46529
47348
|
});
|
|
46530
47349
|
} catch (error) {
|
|
46531
|
-
ctx.log?.(`factory: discovery ${area}: ${errorMessage(error)}`);
|
|
47350
|
+
ctx.log?.(`factory: discovery ${area}: ${errorMessage$1(error)}`);
|
|
46532
47351
|
}
|
|
46533
47352
|
return {
|
|
46534
47353
|
...loop,
|
|
@@ -46592,7 +47411,7 @@ async function startBuilder(ctx, item, vision, loop) {
|
|
|
46592
47411
|
} catch (error) {
|
|
46593
47412
|
ctx.store.updateItem(item.id, (i) => ({
|
|
46594
47413
|
...i,
|
|
46595
|
-
note: `Build not started: ${errorMessage(error)}`
|
|
47414
|
+
note: `Build not started: ${errorMessage$1(error)}`
|
|
46596
47415
|
}));
|
|
46597
47416
|
}
|
|
46598
47417
|
}
|
|
@@ -46667,7 +47486,7 @@ async function triage(ctx, projectId, req) {
|
|
|
46667
47486
|
score: req.score ?? i.score,
|
|
46668
47487
|
finishedAt: at
|
|
46669
47488
|
}));
|
|
46670
|
-
if (item.issue) await ctx.github.comment(projectId, item.issue.number, `The CodeDen factory is not taking this on: ${req.reason}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage(error)}`));
|
|
47489
|
+
if (item.issue) await ctx.github.comment(projectId, item.issue.number, `The CodeDen factory is not taking this on: ${req.reason}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage$1(error)}`));
|
|
46671
47490
|
return rejected;
|
|
46672
47491
|
}
|
|
46673
47492
|
const criteria = req.acceptanceCriteria ?? item.acceptanceCriteria;
|
|
@@ -46675,7 +47494,7 @@ async function triage(ctx, projectId, req) {
|
|
|
46675
47494
|
let issue = item.issue;
|
|
46676
47495
|
if (issue) {
|
|
46677
47496
|
await ctx.github.labelIssue(projectId, issue.number, [FACTORY_LABEL]).catch(() => void 0);
|
|
46678
|
-
await ctx.github.comment(projectId, issue.number, `Accepted by the CodeDen factory. Acceptance criteria:\n${criteria.map((c) => `- [ ] ${c}`).join("\n")}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage(error)}`));
|
|
47497
|
+
await ctx.github.comment(projectId, issue.number, `Accepted by the CodeDen factory. Acceptance criteria:\n${criteria.map((c) => `- [ ] ${c}`).join("\n")}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage$1(error)}`));
|
|
46679
47498
|
} else issue = await ctx.github.createIssue(projectId, {
|
|
46680
47499
|
title: item.title,
|
|
46681
47500
|
body: issueBody(item, criteria),
|
|
@@ -46929,7 +47748,7 @@ var FactorySessions = class {
|
|
|
46929
47748
|
await this.deps.sessions.stop(s.id);
|
|
46930
47749
|
stopped.push(s.id);
|
|
46931
47750
|
} catch (error) {
|
|
46932
|
-
this.deps.log?.(`factory: stop ${s.id}: ${errorMessage(error)}`);
|
|
47751
|
+
this.deps.log?.(`factory: stop ${s.id}: ${errorMessage$1(error)}`);
|
|
46933
47752
|
}
|
|
46934
47753
|
}
|
|
46935
47754
|
return stopped;
|
|
@@ -47072,7 +47891,7 @@ var FactoryService = class {
|
|
|
47072
47891
|
tick(projectId) {
|
|
47073
47892
|
const next = (this.chains.get(projectId) ?? Promise.resolve(void 0)).catch(() => void 0).then(() => runTick(this.ctx, projectId, { digest: (p, l, day) => this.writeDigest(p, l, day) }));
|
|
47074
47893
|
this.chains.set(projectId, next);
|
|
47075
|
-
next.catch((error) => this.deps.log?.(`factory: tick ${projectId}: ${errorMessage(error)}`)).finally(() => {
|
|
47894
|
+
next.catch((error) => this.deps.log?.(`factory: tick ${projectId}: ${errorMessage$1(error)}`)).finally(() => {
|
|
47076
47895
|
if (this.chains.get(projectId) === next) this.chains.delete(projectId);
|
|
47077
47896
|
});
|
|
47078
47897
|
return next;
|
|
@@ -47083,7 +47902,7 @@ var FactoryService = class {
|
|
|
47083
47902
|
try {
|
|
47084
47903
|
projects = this.deps.projects.list();
|
|
47085
47904
|
} catch (error) {
|
|
47086
|
-
this.deps.log?.(`factory: listing projects: ${errorMessage(error)}`);
|
|
47905
|
+
this.deps.log?.(`factory: listing projects: ${errorMessage$1(error)}`);
|
|
47087
47906
|
return;
|
|
47088
47907
|
}
|
|
47089
47908
|
for (const p of projects) {
|
|
@@ -47126,11 +47945,11 @@ var FactoryService = class {
|
|
|
47126
47945
|
shareError: null
|
|
47127
47946
|
};
|
|
47128
47947
|
} catch (error) {
|
|
47129
|
-
this.deps.log?.(`factory: committing the vision: ${errorMessage(error)}`);
|
|
47948
|
+
this.deps.log?.(`factory: committing the vision: ${errorMessage$1(error)}`);
|
|
47130
47949
|
return {
|
|
47131
47950
|
...saved,
|
|
47132
47951
|
pr: null,
|
|
47133
|
-
shareError: errorMessage(error)
|
|
47952
|
+
shareError: errorMessage$1(error)
|
|
47134
47953
|
};
|
|
47135
47954
|
}
|
|
47136
47955
|
}
|
|
@@ -47623,7 +48442,7 @@ var GithubFactoryPort = class {
|
|
|
47623
48442
|
};
|
|
47624
48443
|
});
|
|
47625
48444
|
} catch (error) {
|
|
47626
|
-
throw new Error(`checks for ${sha.slice(0, 7)}: ${errorMessage(error)}`);
|
|
48445
|
+
throw new Error(`checks for ${sha.slice(0, 7)}: ${errorMessage$1(error)}`);
|
|
47627
48446
|
}
|
|
47628
48447
|
}
|
|
47629
48448
|
};
|
|
@@ -51190,7 +52009,7 @@ var GithubClient = class {
|
|
|
51190
52009
|
function toGithubError(error) {
|
|
51191
52010
|
if (error instanceof ServiceError) return error;
|
|
51192
52011
|
const http = httpError(error);
|
|
51193
|
-
if (!http) return new ServiceError(502, "github_error", `GitHub request failed: ${errorMessage(error)}`);
|
|
52012
|
+
if (!http) return new ServiceError(502, "github_error", `GitHub request failed: ${errorMessage$1(error)}`);
|
|
51194
52013
|
const headers = http.response?.headers ?? {};
|
|
51195
52014
|
const remaining = headers["x-ratelimit-remaining"];
|
|
51196
52015
|
const retryAfter = headers["retry-after"];
|
|
@@ -51233,7 +52052,7 @@ var PrFollowUp = class {
|
|
|
51233
52052
|
const { pr } = e.payload;
|
|
51234
52053
|
if (!pr.sessionId) continue;
|
|
51235
52054
|
setImmediate(() => {
|
|
51236
|
-
this.handle(e.type, pr).catch((error) => this.deps.log?.(`github follow-up for PR #${pr.number}: ${errorMessage(error)}`));
|
|
52055
|
+
this.handle(e.type, pr).catch((error) => this.deps.log?.(`github follow-up for PR #${pr.number}: ${errorMessage$1(error)}`));
|
|
51237
52056
|
});
|
|
51238
52057
|
}
|
|
51239
52058
|
}
|
|
@@ -51282,7 +52101,7 @@ var PrFollowUp = class {
|
|
|
51282
52101
|
}, 15e3);
|
|
51283
52102
|
} catch (error) {
|
|
51284
52103
|
const code = runnerErrorCode(error);
|
|
51285
|
-
if (code !== "unknown_session" && code !== "runner_unavailable") this.deps.log?.(`github follow-up: stopping ${sessionId}: ${errorMessage(error)}`);
|
|
52104
|
+
if (code !== "unknown_session" && code !== "runner_unavailable") this.deps.log?.(`github follow-up: stopping ${sessionId}: ${errorMessage$1(error)}`);
|
|
51286
52105
|
}
|
|
51287
52106
|
}
|
|
51288
52107
|
/** Comment on each linked issue that the merged PR resolved it. */
|
|
@@ -51794,7 +52613,7 @@ var GithubSync = class {
|
|
|
51794
52613
|
schedule.nextAt = this.now() + schedule.intervalMs;
|
|
51795
52614
|
} catch (error) {
|
|
51796
52615
|
schedule.failures++;
|
|
51797
|
-
state.lastError = errorMessage(error);
|
|
52616
|
+
state.lastError = errorMessage$1(error);
|
|
51798
52617
|
if (error instanceof GithubRateLimitError) schedule.nextAt = Math.max(error.resetAt, this.now() + 6e4);
|
|
51799
52618
|
else if (error instanceof GithubNotConnectedError) schedule.nextAt = this.now() + 3e5;
|
|
51800
52619
|
else {
|
|
@@ -51932,7 +52751,7 @@ var GithubSync = class {
|
|
|
51932
52751
|
};
|
|
51933
52752
|
});
|
|
51934
52753
|
} catch (error) {
|
|
51935
|
-
this.deps.log?.(`github sync: comments of #${payload.issue.number}: ${errorMessage(error)}`);
|
|
52754
|
+
this.deps.log?.(`github sync: comments of #${payload.issue.number}: ${errorMessage$1(error)}`);
|
|
51936
52755
|
}
|
|
51937
52756
|
}
|
|
51938
52757
|
}
|
|
@@ -52043,7 +52862,7 @@ var GithubService = class {
|
|
|
52043
52862
|
try {
|
|
52044
52863
|
this.reviewInbox.onEvents(events);
|
|
52045
52864
|
} catch (error) {
|
|
52046
|
-
this.deps.log?.(`github: review inbox: ${errorMessage(error)}`);
|
|
52865
|
+
this.deps.log?.(`github: review inbox: ${errorMessage$1(error)}`);
|
|
52047
52866
|
}
|
|
52048
52867
|
});
|
|
52049
52868
|
});
|
|
@@ -52082,7 +52901,7 @@ var GithubService = class {
|
|
|
52082
52901
|
token: token.value
|
|
52083
52902
|
};
|
|
52084
52903
|
} catch (error) {
|
|
52085
|
-
const message = error instanceof ServiceError ? error.message : errorMessage(error);
|
|
52904
|
+
const message = error instanceof ServiceError ? error.message : errorMessage$1(error);
|
|
52086
52905
|
return {
|
|
52087
52906
|
connected: false,
|
|
52088
52907
|
source: token.source,
|
|
@@ -52174,7 +52993,7 @@ var GithubService = class {
|
|
|
52174
52993
|
...req.commitTitle ? { commit_title: req.commitTitle } : {},
|
|
52175
52994
|
...req.commitMessage ? { commit_message: req.commitMessage } : {}
|
|
52176
52995
|
}));
|
|
52177
|
-
await refreshPr(this.prDeps(), projectId, repo, number).catch((error) => this.deps.log?.(`github: refresh after merge: ${errorMessage(error)}`));
|
|
52996
|
+
await refreshPr(this.prDeps(), projectId, repo, number).catch((error) => this.deps.log?.(`github: refresh after merge: ${errorMessage$1(error)}`));
|
|
52178
52997
|
return {
|
|
52179
52998
|
merged: res.data.merged,
|
|
52180
52999
|
sha: res.data.sha ?? null
|
|
@@ -52393,7 +53212,7 @@ var ImageService = class {
|
|
|
52393
53212
|
this.checkedAt = Date.now();
|
|
52394
53213
|
return available;
|
|
52395
53214
|
}).catch((error) => {
|
|
52396
|
-
this.deps.log?.(`images: detection failed: ${errorMessage(error)}`);
|
|
53215
|
+
this.deps.log?.(`images: detection failed: ${errorMessage$1(error)}`);
|
|
52397
53216
|
this.available = false;
|
|
52398
53217
|
this.checkedAt = Date.now();
|
|
52399
53218
|
return false;
|
|
@@ -52437,7 +53256,7 @@ var ImageService = class {
|
|
|
52437
53256
|
try {
|
|
52438
53257
|
target = resolveImageOutput(root, req.outputPath);
|
|
52439
53258
|
} catch (error) {
|
|
52440
|
-
throw badRequest(errorMessage(error), "invalid_output_path");
|
|
53259
|
+
throw badRequest(errorMessage$1(error), "invalid_output_path");
|
|
52441
53260
|
}
|
|
52442
53261
|
this.assertNoEscape(root, target.outputDir);
|
|
52443
53262
|
const references = (req.referenceImages ?? []).map((p) => this.referencePath(cwd, p));
|
|
@@ -52587,7 +53406,7 @@ function toServiceError(error) {
|
|
|
52587
53406
|
const retry = error.details.retryAfter ? ` Try again after ${error.details.retryAfter.toISOString()}.` : "";
|
|
52588
53407
|
return new ServiceError(error.code === "invalid_request" || error.code === "content_policy" ? 400 : 502, `image_${error.code}`, `${error.message}${retry}`);
|
|
52589
53408
|
}
|
|
52590
|
-
return new ServiceError(502, "image_failed", errorMessage(error));
|
|
53409
|
+
return new ServiceError(502, "image_failed", errorMessage$1(error));
|
|
52591
53410
|
}
|
|
52592
53411
|
// ../server/src/services/inbox-service.ts
|
|
52593
53412
|
/** The inbox (§11.1): everything waiting on the user. */
|
|
@@ -53611,7 +54430,7 @@ var OrchestrationService = class {
|
|
|
53611
54430
|
text: parts.join("\n\n")
|
|
53612
54431
|
};
|
|
53613
54432
|
} catch (error) {
|
|
53614
|
-
this.deps.log?.(`orchestration digest: ${errorMessage(error)}`);
|
|
54433
|
+
this.deps.log?.(`orchestration digest: ${errorMessage$1(error)}`);
|
|
53615
54434
|
return input;
|
|
53616
54435
|
}
|
|
53617
54436
|
}
|
|
@@ -53727,7 +54546,7 @@ var OrchestrationService = class {
|
|
|
53727
54546
|
}), ...deriveGithubNotices(events, session)];
|
|
53728
54547
|
for (const { notice, wake } of derived) this.notify(notice, wake);
|
|
53729
54548
|
} catch (error) {
|
|
53730
|
-
this.deps.log?.(`orchestration notices: ${errorMessage(error)}`);
|
|
54549
|
+
this.deps.log?.(`orchestration notices: ${errorMessage$1(error)}`);
|
|
53731
54550
|
}
|
|
53732
54551
|
});
|
|
53733
54552
|
}
|
|
@@ -54236,12 +55055,12 @@ var ProjectRedetector = class {
|
|
|
54236
55055
|
try {
|
|
54237
55056
|
projects = this.deps.projects.list();
|
|
54238
55057
|
} catch (error) {
|
|
54239
|
-
this.deps.log?.(`projects: listing for the start-up check: ${errorMessage(error)}`);
|
|
55058
|
+
this.deps.log?.(`projects: listing for the start-up check: ${errorMessage$1(error)}`);
|
|
54240
55059
|
return;
|
|
54241
55060
|
}
|
|
54242
55061
|
for (const project of projects) {
|
|
54243
55062
|
if (this.stopped) return;
|
|
54244
|
-
await this.refresh(project.id).catch((error) => this.deps.log?.(`projects: checking ${project.name}: ${errorMessage(error)}`));
|
|
55063
|
+
await this.refresh(project.id).catch((error) => this.deps.log?.(`projects: checking ${project.name}: ${errorMessage$1(error)}`));
|
|
54245
55064
|
}
|
|
54246
55065
|
}
|
|
54247
55066
|
onEvents(events) {
|
|
@@ -54257,7 +55076,7 @@ var ProjectRedetector = class {
|
|
|
54257
55076
|
if (projectId) projectIds.add(projectId);
|
|
54258
55077
|
}
|
|
54259
55078
|
} catch (error) {
|
|
54260
|
-
this.deps.log?.(`projects: after a turn: ${errorMessage(error)}`);
|
|
55079
|
+
this.deps.log?.(`projects: after a turn: ${errorMessage$1(error)}`);
|
|
54261
55080
|
return;
|
|
54262
55081
|
}
|
|
54263
55082
|
for (const projectId of projectIds) this.afterTurn(projectId);
|
|
@@ -54276,7 +55095,7 @@ var ProjectRedetector = class {
|
|
|
54276
55095
|
this.again.add(projectId);
|
|
54277
55096
|
return;
|
|
54278
55097
|
}
|
|
54279
|
-
this.refresh(projectId).catch((error) => this.deps.log?.(`projects: checking ${projectId}: ${errorMessage(error)}`));
|
|
55098
|
+
this.refresh(projectId).catch((error) => this.deps.log?.(`projects: checking ${projectId}: ${errorMessage$1(error)}`));
|
|
54280
55099
|
}
|
|
54281
55100
|
};
|
|
54282
55101
|
// ../server/src/services/orchestration/prompts.ts
|
|
@@ -54461,7 +55280,7 @@ var SessionLauncher = class {
|
|
|
54461
55280
|
}
|
|
54462
55281
|
/** Mark a session failed with an inbox item explaining why. */
|
|
54463
55282
|
fail(sessionId, key, error) {
|
|
54464
|
-
const message = errorMessage(error);
|
|
55283
|
+
const message = errorMessage$1(error);
|
|
54465
55284
|
this.deps.log?.(`session ${sessionId}: ${message}`);
|
|
54466
55285
|
if (!this.deps.repo.find(sessionId)) return;
|
|
54467
55286
|
const session = this.deps.repo.update(sessionId, (s) => s.status === "stopped" ? s : {
|
|
@@ -54645,7 +55464,7 @@ var SessionLifecycle = class {
|
|
|
54645
55464
|
title
|
|
54646
55465
|
});
|
|
54647
55466
|
} catch (error) {
|
|
54648
|
-
this.deps.log?.(`utility title: ${errorMessage(error)}`);
|
|
55467
|
+
this.deps.log?.(`utility title: ${errorMessage$1(error)}`);
|
|
54649
55468
|
} finally {
|
|
54650
55469
|
this.titling.delete(sessionId);
|
|
54651
55470
|
}
|
|
@@ -54658,7 +55477,7 @@ var SessionLifecycle = class {
|
|
|
54658
55477
|
try {
|
|
54659
55478
|
this.drainQueue();
|
|
54660
55479
|
} catch (error) {
|
|
54661
|
-
this.deps.log?.(`queue: ${errorMessage(error)}`);
|
|
55480
|
+
this.deps.log?.(`queue: ${errorMessage$1(error)}`);
|
|
54662
55481
|
}
|
|
54663
55482
|
});
|
|
54664
55483
|
}
|
|
@@ -54701,7 +55520,7 @@ var SessionLifecycle = class {
|
|
|
54701
55520
|
})]);
|
|
54702
55521
|
}
|
|
54703
55522
|
} catch (error) {
|
|
54704
|
-
this.deps.log?.(`utility hooks: ${errorMessage(error)}`);
|
|
55523
|
+
this.deps.log?.(`utility hooks: ${errorMessage$1(error)}`);
|
|
54705
55524
|
}
|
|
54706
55525
|
}
|
|
54707
55526
|
};
|
|
@@ -55021,7 +55840,7 @@ var SessionRecovery = class {
|
|
|
55021
55840
|
const status = await runner.request({ name: "runner.status" }, 1e4);
|
|
55022
55841
|
hosted = new Set(status.sessions.map((s) => s.sessionId));
|
|
55023
55842
|
} catch (error) {
|
|
55024
|
-
log?.(`recovery: runner status failed: ${errorMessage(error)}`);
|
|
55843
|
+
log?.(`recovery: runner status failed: ${errorMessage$1(error)}`);
|
|
55025
55844
|
return;
|
|
55026
55845
|
}
|
|
55027
55846
|
for (const id of candidates) {
|
|
@@ -55808,7 +56627,7 @@ var WorktreeCleanup = class {
|
|
|
55808
56627
|
force: opts.force
|
|
55809
56628
|
}, REMOVE_TIMEOUT_MS)).discardedChanges === true;
|
|
55810
56629
|
} catch (error) {
|
|
55811
|
-
const message = errorMessage(error);
|
|
56630
|
+
const message = errorMessage$1(error);
|
|
55812
56631
|
if (runnerErrorCode(error) === "worktree_dirty" || /modified or untracked/.test(message)) return outcome("kept_changes", "The worktree has uncommitted changes, so it was kept");
|
|
55813
56632
|
this.deps.log?.(`worktree cleanup: could not remove ${session.worktreePath}: ${message}`);
|
|
55814
56633
|
return outcome("failed", message);
|
|
@@ -55845,7 +56664,7 @@ var WorktreeCleanup = class {
|
|
|
55845
56664
|
else if (result.status === "kept_changes") this.kept.set(session.id, this.deps.repo.get(session.id).updatedAt);
|
|
55846
56665
|
}
|
|
55847
56666
|
} catch (error) {
|
|
55848
|
-
this.deps.log?.(`worktree cleanup failed: ${errorMessage(error)}`);
|
|
56667
|
+
this.deps.log?.(`worktree cleanup failed: ${errorMessage$1(error)}`);
|
|
55849
56668
|
} finally {
|
|
55850
56669
|
this.sweeping = false;
|
|
55851
56670
|
}
|
|
@@ -55867,7 +56686,7 @@ var WorktreeCleanup = class {
|
|
|
55867
56686
|
for (const e of events) {
|
|
55868
56687
|
if (e.type !== "project.archived") continue;
|
|
55869
56688
|
const projectId = e.payload.projectId;
|
|
55870
|
-
if (typeof projectId === "string") this.cleanProject(projectId).catch((error) => this.deps.log?.(`worktree cleanup for project ${projectId}: ${errorMessage(error)}`));
|
|
56689
|
+
if (typeof projectId === "string") this.cleanProject(projectId).catch((error) => this.deps.log?.(`worktree cleanup for project ${projectId}: ${errorMessage$1(error)}`));
|
|
55871
56690
|
}
|
|
55872
56691
|
}
|
|
55873
56692
|
now() {
|
|
@@ -55922,7 +56741,7 @@ var ClaudeSdkService = class {
|
|
|
55922
56741
|
this.progress = line;
|
|
55923
56742
|
}
|
|
55924
56743
|
}).then(() => importClaudeSdkFrom(this.deps.depsDir)).then(() => this.deps.log?.(`Claude Code support installed (${CLAUDE_SDK_VERSION})`), (error) => {
|
|
55925
|
-
this.error = errorMessage(error);
|
|
56744
|
+
this.error = errorMessage$1(error);
|
|
55926
56745
|
this.deps.log?.(`Claude Code support install failed: ${this.error}`);
|
|
55927
56746
|
}).finally(() => {
|
|
55928
56747
|
this.running = void 0;
|
|
@@ -56476,7 +57295,7 @@ var SetupService = class {
|
|
|
56476
57295
|
authMethod: null,
|
|
56477
57296
|
account: null,
|
|
56478
57297
|
loginExpiresAt: null,
|
|
56479
|
-
message: `Check failed: ${errorMessage(error)}`,
|
|
57298
|
+
message: `Check failed: ${errorMessage$1(error)}`,
|
|
56480
57299
|
fix: null,
|
|
56481
57300
|
durationMs: Math.max(0, this.now() - started)
|
|
56482
57301
|
};
|
|
@@ -56708,7 +57527,7 @@ var DesktopUpdateService = class {
|
|
|
56708
57527
|
await this.deps.desktop.run("check");
|
|
56709
57528
|
this.installFailed = false;
|
|
56710
57529
|
} catch (error) {
|
|
56711
|
-
this.deps.log?.(`updates: check failed: ${errorMessage(error)}`);
|
|
57530
|
+
this.deps.log?.(`updates: check failed: ${errorMessage$1(error)}`);
|
|
56712
57531
|
}
|
|
56713
57532
|
this.emit();
|
|
56714
57533
|
}
|
|
@@ -56717,7 +57536,7 @@ var DesktopUpdateService = class {
|
|
|
56717
57536
|
this.deps.desktop.run(command).catch((error) => {
|
|
56718
57537
|
this.installFailed = true;
|
|
56719
57538
|
this.restartWhenDownloaded = false;
|
|
56720
|
-
this.deps.log?.(`updates: ${command} failed: ${errorMessage(error)}`);
|
|
57539
|
+
this.deps.log?.(`updates: ${command} failed: ${errorMessage$1(error)}`);
|
|
56721
57540
|
this.emit();
|
|
56722
57541
|
});
|
|
56723
57542
|
}
|
|
@@ -57040,7 +57859,7 @@ var UpdateService = class {
|
|
|
57040
57859
|
this.latest = await fetchLatestVersion(this.deps.registry, this.deps.fetch);
|
|
57041
57860
|
this.error = null;
|
|
57042
57861
|
} catch (error) {
|
|
57043
|
-
this.error = errorMessage(error);
|
|
57862
|
+
this.error = errorMessage$1(error);
|
|
57044
57863
|
this.deps.log?.(`updates: ${this.error}`);
|
|
57045
57864
|
} finally {
|
|
57046
57865
|
this.checkedAt = this.now();
|
|
@@ -57076,7 +57895,7 @@ var UpdateService = class {
|
|
|
57076
57895
|
else this.emit();
|
|
57077
57896
|
} catch (error) {
|
|
57078
57897
|
this.state = "failed";
|
|
57079
|
-
this.installError = errorMessage(error);
|
|
57898
|
+
this.installError = errorMessage$1(error);
|
|
57080
57899
|
this.progress = null;
|
|
57081
57900
|
this.deps.log?.(`updates: install failed: ${this.installError}`);
|
|
57082
57901
|
this.emit();
|
|
@@ -58838,7 +59657,7 @@ var LocalSttService = class {
|
|
|
58838
59657
|
};
|
|
58839
59658
|
} catch (error) {
|
|
58840
59659
|
if (opts.signal?.aborted) throw new ServiceError(400, "aborted", "Cancelled");
|
|
58841
|
-
throw new ServiceError(500, "transcription_failed", errorMessage(error));
|
|
59660
|
+
throw new ServiceError(500, "transcription_failed", errorMessage$1(error));
|
|
58842
59661
|
} finally {
|
|
58843
59662
|
this.armIdleUnload();
|
|
58844
59663
|
}
|
|
@@ -58895,8 +59714,8 @@ var LocalSttService = class {
|
|
|
58895
59714
|
job.received = received;
|
|
58896
59715
|
}).catch((error) => {
|
|
58897
59716
|
if (!abort.signal.aborted) {
|
|
58898
|
-
this.errors.set(key, errorMessage(error));
|
|
58899
|
-
this.deps.log?.(`Whisper download (${key}) failed: ${errorMessage(error)}`);
|
|
59717
|
+
this.errors.set(key, errorMessage$1(error));
|
|
59718
|
+
this.deps.log?.(`Whisper download (${key}) failed: ${errorMessage$1(error)}`);
|
|
58900
59719
|
}
|
|
58901
59720
|
throw error;
|
|
58902
59721
|
}).finally(() => this.jobs.delete(key));
|
|
@@ -59377,18 +60196,51 @@ async function startServer(opts) {
|
|
|
59377
60196
|
services.worktreeCleanup.start();
|
|
59378
60197
|
const stopUpdateEvents = services.updates.onChange((status) => hub.broadcastUpdate(status));
|
|
59379
60198
|
services.updates.start();
|
|
59380
|
-
let
|
|
60199
|
+
let baseOrigins = /* @__PURE__ */ new Set();
|
|
59381
60200
|
const app = new Hono();
|
|
60201
|
+
const wss = new import_websocket_server.default({ noServer: true });
|
|
60202
|
+
const httpServer = createAdaptorServer({
|
|
60203
|
+
fetch: app.fetch,
|
|
60204
|
+
websocket: { server: wss }
|
|
60205
|
+
});
|
|
60206
|
+
const listener = new HttpListener(httpServer);
|
|
60207
|
+
const network = new NetworkAccess({
|
|
60208
|
+
store: new RemoteAccessStore(paths.remoteAccess),
|
|
60209
|
+
listener,
|
|
60210
|
+
interfaces: opts.networkInterfaces,
|
|
60211
|
+
securityLogFile: paths.remoteAccessLog,
|
|
60212
|
+
log
|
|
60213
|
+
});
|
|
60214
|
+
const sessionCookie = () => `codeden_session_${port}`;
|
|
60215
|
+
const tokenCookie = () => `codeden_token_${port}`;
|
|
60216
|
+
const allowedOrigins = () => {
|
|
60217
|
+
const own = [...network.ownHosts()].map((h) => `http://${h}:${port}`);
|
|
60218
|
+
return own.length === 0 ? baseOrigins : /* @__PURE__ */ new Set([...baseOrigins, ...own]);
|
|
60219
|
+
};
|
|
59382
60220
|
app.use("*", securityMiddleware({
|
|
59383
60221
|
token: opts.token,
|
|
59384
|
-
cookieName:
|
|
59385
|
-
allowedOrigins
|
|
59386
|
-
publicPaths: /* @__PURE__ */ new Set(["/api/health", "/mcp"])
|
|
60222
|
+
cookieName: tokenCookie,
|
|
60223
|
+
allowedOrigins,
|
|
60224
|
+
publicPaths: /* @__PURE__ */ new Set(["/api/health", "/mcp"]),
|
|
60225
|
+
loopbackOnlyPaths: /* @__PURE__ */ new Set(["/mcp"]),
|
|
60226
|
+
remote: {
|
|
60227
|
+
enabled: () => network.enabled,
|
|
60228
|
+
ownHosts: () => network.ownHosts(),
|
|
60229
|
+
port: () => port,
|
|
60230
|
+
sessionCookieName: sessionCookie,
|
|
60231
|
+
verifySession: (token) => network.store.verifySession(token)
|
|
60232
|
+
}
|
|
59387
60233
|
}));
|
|
59388
60234
|
const api = createApi({
|
|
59389
60235
|
services,
|
|
59390
60236
|
runnerStatus,
|
|
59391
60237
|
startedAt,
|
|
60238
|
+
network: {
|
|
60239
|
+
network,
|
|
60240
|
+
sessionCookie,
|
|
60241
|
+
tokenCookie,
|
|
60242
|
+
token: opts.token
|
|
60243
|
+
},
|
|
59392
60244
|
log,
|
|
59393
60245
|
shutdown: ({ stopRunner }) => {
|
|
59394
60246
|
setTimeout(() => {
|
|
@@ -59438,18 +60290,9 @@ async function startServer(opts) {
|
|
|
59438
60290
|
return upgradeWebSocket(() => browserBridge.handlers(query.data))(c, next);
|
|
59439
60291
|
});
|
|
59440
60292
|
mountWeb(app, findWebDir(opts.webDir));
|
|
59441
|
-
|
|
59442
|
-
|
|
59443
|
-
|
|
59444
|
-
fetch: app.fetch,
|
|
59445
|
-
port: opts.port,
|
|
59446
|
-
hostname: "127.0.0.1",
|
|
59447
|
-
websocket: { server: wss }
|
|
59448
|
-
}, () => resolve(s));
|
|
59449
|
-
s.once("error", reject);
|
|
59450
|
-
});
|
|
59451
|
-
port = server.address().port;
|
|
59452
|
-
origins = /* @__PURE__ */ new Set([...loopbackOrigins(port), ...opts.allowedOrigins ?? []]);
|
|
60293
|
+
if (network.store.enabled && !network.store.passwordSet) log("network access is on but no password is set: listening on 127.0.0.1 only");
|
|
60294
|
+
port = await listener.listen(opts.port, network.desiredHost());
|
|
60295
|
+
baseOrigins = /* @__PURE__ */ new Set([...loopbackOrigins(port), ...opts.allowedOrigins ?? []]);
|
|
59453
60296
|
const url = `http://127.0.0.1:${port}`;
|
|
59454
60297
|
const lock = {
|
|
59455
60298
|
pid: process.pid,
|
|
@@ -59459,6 +60302,7 @@ async function startServer(opts) {
|
|
|
59459
60302
|
};
|
|
59460
60303
|
writeJsonFile(paths.serverLock, lock);
|
|
59461
60304
|
log(`server listening on ${url}`);
|
|
60305
|
+
if (network.enabled) log(`network access on: ${network.addresses().map((a) => `${a.label} ${a.url}`).join(", ")}`);
|
|
59462
60306
|
if (opts.checkSetupOnStart !== false) services.setup.status({ refresh: true }).catch((error) => log(`setup: ${String(error)}`));
|
|
59463
60307
|
if (opts.runner !== false) {
|
|
59464
60308
|
const args = [...opts.enableFakeProvider ? ["--fake"] : [], ...builtin ? [] : ["--no-builtin"]];
|
|
@@ -59503,10 +60347,8 @@ async function startServer(opts) {
|
|
|
59503
60347
|
}
|
|
59504
60348
|
hub.close();
|
|
59505
60349
|
wss.close();
|
|
59506
|
-
await
|
|
59507
|
-
|
|
59508
|
-
if ("closeAllConnections" in server) server.closeAllConnections();
|
|
59509
|
-
});
|
|
60350
|
+
await listener.close();
|
|
60351
|
+
if ("closeAllConnections" in httpServer) httpServer.closeAllConnections();
|
|
59510
60352
|
database.close();
|
|
59511
60353
|
rmSync(paths.serverLock, { force: true });
|
|
59512
60354
|
})();
|
|
@@ -59521,6 +60363,7 @@ async function startServer(opts) {
|
|
|
59521
60363
|
database,
|
|
59522
60364
|
services,
|
|
59523
60365
|
runner,
|
|
60366
|
+
network,
|
|
59524
60367
|
close
|
|
59525
60368
|
};
|
|
59526
60369
|
}
|
|
@@ -59719,6 +60562,147 @@ function versionRows(home, launch) {
|
|
|
59719
60562
|
}
|
|
59720
60563
|
return rows;
|
|
59721
60564
|
}
|
|
60565
|
+
// src/network-command.ts
|
|
60566
|
+
const DEFAULT_PORT$1 = 4317;
|
|
60567
|
+
/** Call the running server's network API over loopback with the install token. */
|
|
60568
|
+
async function callServer(home, path, init) {
|
|
60569
|
+
const paths = ensureHome(home);
|
|
60570
|
+
const running = await findRunningServer(paths.serverLock);
|
|
60571
|
+
if (!running) return void 0;
|
|
60572
|
+
const token = ensureInstallToken(paths.token);
|
|
60573
|
+
const res = await fetch(`http://127.0.0.1:${running.port}${path}`, {
|
|
60574
|
+
method: init.method,
|
|
60575
|
+
headers: {
|
|
60576
|
+
authorization: `Bearer ${token}`,
|
|
60577
|
+
"content-type": "application/json"
|
|
60578
|
+
},
|
|
60579
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
60580
|
+
});
|
|
60581
|
+
return {
|
|
60582
|
+
ok: res.ok,
|
|
60583
|
+
status: res.status,
|
|
60584
|
+
body: await res.json().catch(() => null)
|
|
60585
|
+
};
|
|
60586
|
+
}
|
|
60587
|
+
function errorMessage(body) {
|
|
60588
|
+
const message = body?.error?.message;
|
|
60589
|
+
return typeof message === "string" ? message : "The server refused the request.";
|
|
60590
|
+
}
|
|
60591
|
+
function printAddresses(addresses) {
|
|
60592
|
+
for (const a of addresses) console.log(` ${a.label.padEnd(16)} ${a.url}`);
|
|
60593
|
+
}
|
|
60594
|
+
/** Ask for a line without echoing it (a TTY), or read the first line of piped input. */
|
|
60595
|
+
async function readSecret(question) {
|
|
60596
|
+
const tty = process.stdin.isTTY === true;
|
|
60597
|
+
const rl = createInterface({
|
|
60598
|
+
input: process.stdin,
|
|
60599
|
+
output: process.stdout,
|
|
60600
|
+
terminal: tty
|
|
60601
|
+
});
|
|
60602
|
+
if (tty) {
|
|
60603
|
+
process.stdout.write(question);
|
|
60604
|
+
rl._writeToOutput = () => {};
|
|
60605
|
+
}
|
|
60606
|
+
return new Promise((resolve) => {
|
|
60607
|
+
rl.once("line", (line) => {
|
|
60608
|
+
if (tty) process.stdout.write("\n");
|
|
60609
|
+
rl.close();
|
|
60610
|
+
resolve(line);
|
|
60611
|
+
});
|
|
60612
|
+
rl.once("close", () => resolve(""));
|
|
60613
|
+
});
|
|
60614
|
+
}
|
|
60615
|
+
/**
|
|
60616
|
+
* `codeden password [--generate]`: set the network-access password (signs out every remote
|
|
60617
|
+
* device). Through the running server when there is one, else straight into the home.
|
|
60618
|
+
*/
|
|
60619
|
+
async function passwordCommand(home, opts) {
|
|
60620
|
+
let password = null;
|
|
60621
|
+
if (!opts.generate) {
|
|
60622
|
+
console.log(PASSWORD_RULE_TEXT);
|
|
60623
|
+
password = await readSecret("New password: ");
|
|
60624
|
+
const problem = passwordProblem(password);
|
|
60625
|
+
if (problem) {
|
|
60626
|
+
console.error(problem);
|
|
60627
|
+
return 1;
|
|
60628
|
+
}
|
|
60629
|
+
if (process.stdin.isTTY && await readSecret("Repeat it: ") !== password) {
|
|
60630
|
+
console.error("The two passwords don't match.");
|
|
60631
|
+
return 1;
|
|
60632
|
+
}
|
|
60633
|
+
}
|
|
60634
|
+
const viaServer = await callServer(home, "/api/network/password", {
|
|
60635
|
+
method: "POST",
|
|
60636
|
+
body: password === null ? { generate: true } : { password }
|
|
60637
|
+
});
|
|
60638
|
+
let generated;
|
|
60639
|
+
if (viaServer) {
|
|
60640
|
+
if (!viaServer.ok) {
|
|
60641
|
+
console.error(errorMessage(viaServer.body));
|
|
60642
|
+
return 1;
|
|
60643
|
+
}
|
|
60644
|
+
generated = SetNetworkPasswordResponse.parse(viaServer.body).generated;
|
|
60645
|
+
} else {
|
|
60646
|
+
const store = new RemoteAccessStore(ensureHome(home).remoteAccess);
|
|
60647
|
+
generated = password === null ? generatePassphrase() : null;
|
|
60648
|
+
try {
|
|
60649
|
+
await store.setPassword(generated ?? password);
|
|
60650
|
+
} catch (error) {
|
|
60651
|
+
if (error instanceof PasswordRuleError) {
|
|
60652
|
+
console.error(error.message);
|
|
60653
|
+
return 1;
|
|
60654
|
+
}
|
|
60655
|
+
throw error;
|
|
60656
|
+
}
|
|
60657
|
+
}
|
|
60658
|
+
if (generated) {
|
|
60659
|
+
console.log("Your network access password (shown once, keep it somewhere safe):");
|
|
60660
|
+
console.log(` ${generated}`);
|
|
60661
|
+
}
|
|
60662
|
+
console.log("Password saved. Every remote device was signed out.");
|
|
60663
|
+
return 0;
|
|
60664
|
+
}
|
|
60665
|
+
/** `codeden network [on|off]`: switch network access, or show its state. */
|
|
60666
|
+
async function networkCommand(home, action) {
|
|
60667
|
+
if (action !== void 0 && action !== "on" && action !== "off") {
|
|
60668
|
+
console.error("Usage: codeden network [on|off]");
|
|
60669
|
+
return 2;
|
|
60670
|
+
}
|
|
60671
|
+
const viaServer = await callServer(home, "/api/network", action === void 0 ? { method: "GET" } : {
|
|
60672
|
+
method: "PUT",
|
|
60673
|
+
body: { enabled: action === "on" }
|
|
60674
|
+
});
|
|
60675
|
+
if (viaServer) {
|
|
60676
|
+
if (!viaServer.ok) {
|
|
60677
|
+
console.error(errorMessage(viaServer.body));
|
|
60678
|
+
if (viaServer.status === 400) console.error("Run: codeden password");
|
|
60679
|
+
return 1;
|
|
60680
|
+
}
|
|
60681
|
+
const status = NetworkStatus.parse(viaServer.body);
|
|
60682
|
+
if (status.enabled) {
|
|
60683
|
+
console.log("Network access is on. Open CodeDen on your phone at:");
|
|
60684
|
+
printAddresses(status.addresses);
|
|
60685
|
+
} else console.log("Network access is off: CodeDen only answers on this computer (127.0.0.1).");
|
|
60686
|
+
return 0;
|
|
60687
|
+
}
|
|
60688
|
+
const store = new RemoteAccessStore(ensureHome(home).remoteAccess);
|
|
60689
|
+
if (action === void 0) {
|
|
60690
|
+
console.log(`Network access is ${store.enabled && store.passwordSet ? "on" : "off"} (CodeDen is not running).`);
|
|
60691
|
+
return 0;
|
|
60692
|
+
}
|
|
60693
|
+
try {
|
|
60694
|
+
store.setEnabled(action === "on");
|
|
60695
|
+
} catch (error) {
|
|
60696
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
60697
|
+
console.error("Run: codeden password");
|
|
60698
|
+
return 1;
|
|
60699
|
+
}
|
|
60700
|
+
if (action === "on") {
|
|
60701
|
+
console.log("Network access is on. When CodeDen starts it will answer on:");
|
|
60702
|
+
printAddresses(listAddresses(DEFAULT_PORT$1));
|
|
60703
|
+
} else console.log("Network access is off.");
|
|
60704
|
+
return 0;
|
|
60705
|
+
}
|
|
59722
60706
|
// src/open-browser.ts
|
|
59723
60707
|
/** Open a URL in the default browser without a shell (no quoting pitfalls). */
|
|
59724
60708
|
function openBrowser(url) {
|
|
@@ -60071,6 +61055,8 @@ async function viaServer(port, token, rollback) {
|
|
|
60071
61055
|
* codeden doctor [--install]
|
|
60072
61056
|
* codeden update [--rollback]
|
|
60073
61057
|
* codeden stop
|
|
61058
|
+
* codeden password [--generate]
|
|
61059
|
+
* codeden network [on|off]
|
|
60074
61060
|
* Loaded by `cli.ts` once the launcher has picked the version to run.
|
|
60075
61061
|
*/
|
|
60076
61062
|
const DEFAULT_PORT = 4317;
|
|
@@ -60081,6 +61067,8 @@ Usage:
|
|
|
60081
61067
|
codeden doctor Check the installation (--install: add Claude Code support now)
|
|
60082
61068
|
codeden update Install the latest version and switch to it (--rollback: go back)
|
|
60083
61069
|
codeden stop Stop the server and the runner
|
|
61070
|
+
codeden password Set the network access password (--generate: make one up)
|
|
61071
|
+
codeden network on|off Let phones on your network open CodeDen, or stop it
|
|
60084
61072
|
|
|
60085
61073
|
Options:
|
|
60086
61074
|
--port <n> Port to listen on (default ${DEFAULT_PORT})
|
|
@@ -60121,6 +61109,10 @@ async function main() {
|
|
|
60121
61109
|
type: "boolean",
|
|
60122
61110
|
default: false
|
|
60123
61111
|
},
|
|
61112
|
+
generate: {
|
|
61113
|
+
type: "boolean",
|
|
61114
|
+
default: false
|
|
61115
|
+
},
|
|
60124
61116
|
version: {
|
|
60125
61117
|
type: "boolean",
|
|
60126
61118
|
short: "v",
|
|
@@ -60157,6 +61149,8 @@ async function main() {
|
|
|
60157
61149
|
rollback: values.rollback,
|
|
60158
61150
|
launch
|
|
60159
61151
|
});
|
|
61152
|
+
if (command === "password") return passwordCommand(home, { generate: values.generate });
|
|
61153
|
+
if (command === "network") return networkCommand(home, positionals[1]);
|
|
60160
61154
|
if (command) {
|
|
60161
61155
|
console.error(`Unknown command "${command}".\n\n${HELP}`);
|
|
60162
61156
|
return 2;
|
|
@@ -60216,6 +61210,10 @@ async function main() {
|
|
|
60216
61210
|
}
|
|
60217
61211
|
console.log(`CodeDen ${CODEDEN_VERSION} is running at ${server.url}`);
|
|
60218
61212
|
console.log(`Open: ${server.launchUrl}`);
|
|
61213
|
+
if (server.network.enabled) {
|
|
61214
|
+
console.log("Network access is on. Open CodeDen on your phone or tablet at:");
|
|
61215
|
+
for (const a of server.network.addresses()) console.log(` ${a.label.padEnd(16)} ${a.url}`);
|
|
61216
|
+
}
|
|
60219
61217
|
console.log("Press Ctrl+C to stop the server (agents keep running in the runner).");
|
|
60220
61218
|
if (!values["no-open"]) openBrowser(server.launchUrl);
|
|
60221
61219
|
process.on("SIGINT", shutdown);
|
|
@@ -60228,4 +61226,4 @@ main().then((code) => process.exit(code), (error) => {
|
|
|
60228
61226
|
});
|
|
60229
61227
|
export {};
|
|
60230
61228
|
|
|
60231
|
-
//# sourceMappingURL=main-
|
|
61229
|
+
//# sourceMappingURL=main-f5CBW7t4.js.map
|