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.
Files changed (35) hide show
  1. package/README.md +8 -2
  2. package/dist/cli.js +1 -1
  3. package/dist/{main-DgpYK_Si.js → main-Bubch4fJ.js} +2 -2
  4. package/dist/{main-DgpYK_Si.js.map → main-Bubch4fJ.js.map} +1 -1
  5. package/dist/{main-BAX1sPiD.js → main-f5CBW7t4.js} +1196 -198
  6. package/dist/main-f5CBW7t4.js.map +1 -0
  7. package/dist/{runner-DGomZl80.js → runner-Doy0yaNT.js} +198 -3
  8. package/dist/runner-Doy0yaNT.js.map +1 -0
  9. package/dist/runner.js +1 -1
  10. package/dist/web/apple-touch-icon.png +0 -0
  11. package/dist/web/assets/{core-D2h_xdbh.js → core-BnICSmTX.js} +1 -1
  12. package/dist/web/assets/engine-oniguruma-Tz65MLLK.js +1 -0
  13. package/dist/web/assets/{highlighted-body-JLMTFACH-DpId3Lg2.js → highlighted-body-JLMTFACH-DiAsPNHI.js} +1 -1
  14. package/dist/web/assets/{highlights-C1SeynWZ.js → highlights-mfetlXdJ.js} +1 -1
  15. package/dist/web/assets/index-CXjq9NOS.js +914 -0
  16. package/dist/web/assets/index-DcErU_f1.css +1 -0
  17. package/dist/web/assets/langs-B69Lxr5B.js +1 -0
  18. package/dist/web/assets/mermaid-WFAXDIZX-CbtYONW-.js +1 -0
  19. package/dist/web/assets/{shiki-h6vxFpD2.js → shiki-B1ZU2vss.js} +2 -2
  20. package/dist/web/assets/{stream-tokenizer-D8Euixi9.js → stream-tokenizer-1i17e6mT.js} +1 -1
  21. package/dist/web/assets/{themes-Bi1mPEwQ.js → themes-CmE23Ko0.js} +2 -2
  22. package/dist/web/icons/icon-192.png +0 -0
  23. package/dist/web/icons/icon-512.png +0 -0
  24. package/dist/web/icons/maskable-192.png +0 -0
  25. package/dist/web/icons/maskable-512.png +0 -0
  26. package/dist/web/index.html +10 -2
  27. package/dist/web/manifest.webmanifest +28 -0
  28. package/package.json +1 -1
  29. package/dist/main-BAX1sPiD.js.map +0 -1
  30. package/dist/runner-DGomZl80.js.map +0 -1
  31. package/dist/web/assets/engine-oniguruma-CG0oBFVI.js +0 -1
  32. package/dist/web/assets/index-BPuGYbOr.css +0 -1
  33. package/dist/web/assets/index-BXhyz2mO.js +0 -914
  34. package/dist/web/assets/langs-NwkcQ-R2.js +0 -1
  35. 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 ExportAutomationsQuery, $n as datetime, $t as TriageIssueRequest, A as SessionFileQuery, An as SessionOptions, At as UiClientMessage, B as ImageTestRequest, Bn as InboxItemKind, Bt as UpdateInstallRequest, C as isElectronRuntime, Cn as GitPushRequest, Ct as SecretKey, D as CODEDEN_VERSION, Dn as TerminalClientMessage, Dt as StartSessionRequest, E as wrapper_default, En as SpeechSummaryRequest, Et as SetSecretRequest, F as ListConflictsQuery, Fn as Project, Ft as LOCAL_STT_SAMPLE_RATE, G as BrowserNavigateRequest, Gn as AgentConfigLocator, Gt as NOTICE_BATCH_CODE, H as BrowserClientMessage, Hn as AgentConfigDeleteQuery, Ht as compareVersions, I as ListNoticesQuery, In as sortProjects, It as LocalSttModelParams, J as BrowserViewportRequest, Jn as McpServerTarget, Jt as GithubIssue, K as BrowserNewTabRequest, Kn as AgentConfigWriteRequest, Kt as CreateGithubIssueRequest, L as ResolveConflictRequest, Ln as DEFAULT_VISION_PATH, Lt as SttQuery, M as RunnerToServerMessage, Mn as ConflictDecision, Mt as HealthResponse, N as ServerLock, Nn as StartedBy, Nt as agentEventToNewEvent, O as UsageQuery, On as TerminalQuery, Ot as SteerRequest, P as ArchiveSessionRequest, Pn as WorkKey, Pt as Settings, Q as DuplicateAutomationRequest, Qn as ReasoningEffort, Qt as MergePrRequest, R as RoleMessageRequest, Rn as FactoryLimits, Rt as TtsRequest, S as acquireInstallLock, Sn as GitCommitRequest, St as RespondRequest, T as import_websocket_server, Tn as SessionResult, Tt as SessionStartMeta, U as BrowserDevServerRequest, Un as AgentConfigDiffRequest, Ut as isNewerVersion, V as summarizeActivity, Vn as messageFrom, Vt as UpdateStatus, W as BrowserHistoryRequest, Wn as AgentConfigListQuery, Wt as ConflictRecord, X as AUTOMATION_IMPORT_LIMITS, Xn as AutonomyLevel, Xt as GithubListQuery, Y as BrowserWsQuery, Yn as McpServerUpsertRequest, Yt as GithubLabelRequest, Z as AutomationImportSource, Zn as ProviderId, Zt as GithubPr, _ as importClaudeSdkFrom, _n as TriagePolicy, _r as unknown, _t as FileWriteRequest, a as Hono, an as FactoryTuning, ar as discriminatedUnion, at as ListAutomationRunsQuery, b as runNpm, bn as DiffScope, br as safeParse$1, bt as ReorderProjectsRequest, c as serve, cn as ReviewVerdict, cr as literal, ct as SetAutomationEnabledRequest, d as createImageEngineRegistry, dn as Artifact, dr as object, dt as TriageReport, en as githubPrId, er as _enum, et as ImportAutomationsRequest, f as IMAGE_ASPECT_RATIOS, fn as Automation, fr as optional, ft as UpcomingRunsQuery, g as CLAUDE_SDK_VERSION, gn as AutomationTrigger, gr as union$1, gt as FileReadQuery, h as claudeSdkSource, hn as AutomationRun, hr as string, ht as CreateProjectRequest, i as killTree$1, in as FactoryTriageRequest, ir as custom, it as CreateAutomationRequest, j as RunnerLock, jn as ProjectConfigFile, jt as UpdateProjectRequest, k as UsageSettings, kn as RunnerCommandResults, kt as ThreadItemsQuery, l as upgradeWebSocket, ln as SetAutonomyModeRequest, lr as looseObject, lt as StarterSetRequest, m as ImageGenerationError, mn as AutomationGuards, mr as record, mt as UpdateAutomationRequest, n as readLock, nn as DiscoveryArea, nr as array, nt as AutomationGuardsPatch, o as tryDecodeURIComponent, on as KillSwitchRequest, or as int, ot as ListAutomationsQuery, p as resolveImageOutput, pn as AutomationAction, pr as preprocess, pt as UpdateAutomationProjectSettingsRequest, q as BrowserOpenRequest, qn as McpServerDiffRequest, qt as GithubCommentRequest, r as isAlive, rn as FactoryCandidate, rr as boolean, rt as AutomationProjectRequest, s as bufferToFormData, sn as PutVisionRequest, sr as intersection, st as RunAutomationRequest, tn as parseGithubPrId, tr as _null, tt as AutomationFileFrontmatter, u as ImageEngineRegistry, un as isFactoryMode, ur as number, ut as TriageDecisionRequest, v as installClaudeSdk, vn as Session, vr as ZodError, vt as ListInboxQuery, w as ownNodeEnv, wn as RestoreFilesRequest, wt as SendMessageRequest, x as resolveNpm, xn as EditQueuedMessageRequest, xr as prettifyError, xt as ResolveInboxRequest, y as isClaudeSdkInstalled, yn as SessionStatus, yr as toJSONSchema, yt as ListSessionsQuery, z as UpdateSessionRequest, zn as InboxItem, zt as AppPointer } from "./runner-DGomZl80.js";
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 origins = /* @__PURE__ */ new Set();
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: () => `codeden_token_${port}`,
59385
- allowedOrigins: () => origins,
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
- const wss = new import_websocket_server.default({ noServer: true });
59442
- const server = await new Promise((resolve, reject) => {
59443
- const s = serve({
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 new Promise((resolve) => {
59507
- server.close(() => resolve());
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-BAX1sPiD.js.map
61229
+ //# sourceMappingURL=main-f5CBW7t4.js.map