@cjhyy/code-shell-server 0.9.26 → 0.9.28

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.
@@ -1,4 +1,4 @@
1
- export type { LinkAuthorization, LinkConnectionInput, LinkErrorCode, LinkSnapshot, MaskedLinkConnection, TokenConnectionInput, } from "@cjhyy/code-shell-link";
1
+ export type { LinkAuthorization, LinkAuthMode, LinkAuthorizationInput, LinkAuthorizationResponse, LinkAuthorizationStep, LinkConnectionInput, LinkErrorCode, LinkSnapshot, MaskedLinkConnection, TokenConnectionInput, } from "@cjhyy/code-shell-link";
2
2
  export interface LinkOperationContext {
3
3
  ownerId: string;
4
4
  /** Rechecked after every external await and immediately before persistence. */
@@ -2,6 +2,7 @@ import { request as httpRequest } from "node:http";
2
2
  import { Transform } from "node:stream";
3
3
  import { WebSocket, WebSocketServer } from "ws";
4
4
  import { HUB_SESSION_COOKIE } from "../hub/auth-http.js";
5
+ import { createLinkBrowserHandoff } from "../links/browser-handoff.js";
5
6
  const UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
6
7
  const PROJECT_PATH = new RegExp(`^/p/(${UUID})(/.*)$`);
7
8
  const API_PATH = /^\/api\/v1\/(?:configuration|mcp|skills|links|panels|panel-assets|files|sessions|uploads)(?:\/|$)/;
@@ -53,6 +54,7 @@ class ProxyError extends Error {
53
54
  }
54
55
  /** Private runtime credentials and cookies never cross this control-plane boundary. */
55
56
  export function createProjectRuntimeProxy(options) {
57
+ const linkBrowser = createLinkBrowserHandoff();
56
58
  const leases = new Map();
57
59
  const assets = new Map();
58
60
  const revokedOwners = new Set();
@@ -249,10 +251,10 @@ export function createProjectRuntimeProxy(options) {
249
251
  }
250
252
  return result;
251
253
  }
252
- async function innerJson(target, path, body, cookie, timeout = 10_000) {
254
+ async function innerJson(target, path, body, cookie, timeout = 10_000, method = "POST") {
253
255
  return await new Promise((resolve, reject) => {
254
256
  const request = httpRequest(new URL(path, target.url), {
255
- method: "POST",
257
+ method,
256
258
  headers: {
257
259
  origin: options.publicOrigin(),
258
260
  "content-type": "application/json",
@@ -338,12 +340,24 @@ export function createProjectRuntimeProxy(options) {
338
340
  const renew = req.method === "POST" &&
339
341
  incoming.statusCode === 200 &&
340
342
  /^\/api\/v1\/panels\/runtime\/([A-Za-z0-9_-]+)\/renew$/.exec(route.pathname);
341
- const bytes = prepare || renew ? await readBounded(incoming, MAX_PREPARE_BYTES) : undefined;
343
+ const linkCatalog = req.method === "GET" &&
344
+ route.pathname === "/api/v1/links" &&
345
+ incoming.statusCode === 200;
346
+ let bytes = prepare || renew || linkCatalog
347
+ ? await readBounded(incoming, MAX_PREPARE_BYTES)
348
+ : undefined;
342
349
  if (bytes) {
343
350
  await validate(lease);
344
351
  if (settled)
345
352
  return;
346
- if (prepare)
353
+ if (linkCatalog) {
354
+ const snapshot = JSON.parse(bytes.toString("utf8"));
355
+ if (snapshot.capabilities?.authorizationSteps === 1 &&
356
+ snapshot.capabilities?.remoteAuth === true)
357
+ snapshot.capabilities.browserHandoff = 1;
358
+ bytes = Buffer.from(JSON.stringify(snapshot));
359
+ }
360
+ else if (prepare)
347
361
  rememberPrepare(lease, bytes);
348
362
  else if (renew) {
349
363
  const { expiresAt } = JSON.parse(bytes.toString("utf8"));
@@ -361,6 +375,8 @@ export function createProjectRuntimeProxy(options) {
361
375
  (incoming.statusCode ?? 500) < 300)
362
376
  forgetAssets(lease, route.pathname.split("/").at(-1));
363
377
  for (const name of RESPONSE_HEADERS) {
378
+ if (linkCatalog && name === "content-length")
379
+ continue;
364
380
  const value = incoming.headers[name];
365
381
  if (value !== undefined)
366
382
  res.setHeader(name, value);
@@ -383,6 +399,8 @@ export function createProjectRuntimeProxy(options) {
383
399
  });
384
400
  }
385
401
  async function handle(req, res) {
402
+ if (await linkBrowser.handle(req, res))
403
+ return true;
386
404
  if (!(req.url ?? "").startsWith("/p/"))
387
405
  return false;
388
406
  try {
@@ -394,6 +412,43 @@ export function createProjectRuntimeProxy(options) {
394
412
  : await acquire(req, route.projectId);
395
413
  if (req.aborted || res.destroyed)
396
414
  return true;
415
+ const handoff = /^\/api\/v1\/links\/authorizations\/([a-f0-9-]{36})\/browser$/.exec(route.pathname);
416
+ if (handoff && req.method === "POST") {
417
+ const input = JSON.parse((await readBounded(req, 1024)).toString("utf8"));
418
+ if (!input ||
419
+ Array.isArray(input) ||
420
+ typeof input !== "object" ||
421
+ Object.keys(input).length)
422
+ throw new ProxyError(400, "Invalid browser authorization");
423
+ const target = `/api/v1/links/authorizations/${handoff[1]}`;
424
+ const readJob = async () => {
425
+ await validate(lease);
426
+ const result = await innerJson(lease.target, target, "", lease.cookie, 10_000, "GET");
427
+ await validate(lease);
428
+ const job = JSON.parse(result.bytes.toString("utf8"));
429
+ if (job.id !== handoff[1] || job.state !== "pending")
430
+ throw new ProxyError(409, "Authorization is no longer pending");
431
+ return job;
432
+ };
433
+ const launch = linkBrowser.create(await readJob(), {
434
+ check: async () => {
435
+ await readJob();
436
+ },
437
+ complete: async (callbackUrl) => {
438
+ await validate(lease);
439
+ const result = await innerJson(lease.target, target + "/complete", JSON.stringify({ callbackUrl }), lease.cookie, 30_000);
440
+ await validate(lease);
441
+ return JSON.parse(result.bytes.toString("utf8"));
442
+ },
443
+ cancel: async () => {
444
+ await validate(lease);
445
+ return innerJson(lease.target, target, "", lease.cookie, 10_000, "DELETE");
446
+ },
447
+ }, options.publicOrigin());
448
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
449
+ res.end(JSON.stringify(launch));
450
+ return true;
451
+ }
397
452
  await forwardHttp(req, res, route, lease);
398
453
  }
399
454
  catch (error) {
@@ -588,6 +643,7 @@ export function createProjectRuntimeProxy(options) {
588
643
  revokeProject,
589
644
  async close() {
590
645
  closed = true;
646
+ linkBrowser.close();
591
647
  clearInterval(timer);
592
648
  for (const cancel of upgrades)
593
649
  cancel();
@@ -22,6 +22,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
22
22
  import { randomBytes, randomUUID } from "node:crypto";
23
23
  import { WebSocketServer } from "ws";
24
24
  import { SessionManager, SettingsManager } from "@cjhyy/code-shell-core";
25
+ import { readOutputJournal, readOutputJournalLegacyBase } from "@cjhyy/code-shell-core/internal";
25
26
  import { AccessPasscode } from "../mobile-remote/access-passcode.js";
26
27
  import { resolveSafe } from "../mobile-remote/mobile-static.js";
27
28
  import { contentTypeFor } from "../static-files.js";
@@ -450,6 +451,7 @@ export async function startHeadlessServer(opts) {
450
451
  links = hubAuth
451
452
  ? createLinkHttp({
452
453
  ...configurationOptions,
454
+ browserHandoff: !opts.publicPathPrefix,
453
455
  remoteLink: () => remoteLink,
454
456
  onChanged: () => notify("serve/configurationChanged", {}),
455
457
  })
@@ -563,6 +565,8 @@ export async function startHeadlessServer(opts) {
563
565
  return;
564
566
  }
565
567
  if (hubAuth) {
568
+ if (await links?.handlePublic(req, res))
569
+ return;
566
570
  if (await panels?.handleAssets(req, res))
567
571
  return;
568
572
  if (await hubAuth.handle(req, res))
@@ -1131,7 +1135,7 @@ function replyToHostSessionQuery(message, sessionManager, workspaceCwd, isRunnin
1131
1135
  result: { type: "sessions", data: sessions },
1132
1136
  });
1133
1137
  }
1134
- if (queryType !== "session_detail")
1138
+ if (queryType !== "session_detail" && queryType !== "output_journal")
1135
1139
  return undefined;
1136
1140
  const sessionId = message.params?.sessionId;
1137
1141
  if (typeof sessionId !== "string" || sessionId.length === 0) {
@@ -1146,7 +1150,28 @@ function replyToHostSessionQuery(message, sessionManager, workspaceCwd, isRunnin
1146
1150
  if (resolve(state.cwd) !== workspaceCwd) {
1147
1151
  return hostQueryError(message.id, -32001, "Session not found in this workspace");
1148
1152
  }
1149
- const transcript = readHubTranscript(sessionManager.getStorageDir(), sessionId);
1153
+ if (queryType === "output_journal") {
1154
+ const data = readOutputJournal(sessionManager.getStorageDir(), sessionId, {
1155
+ after: message.params?.after,
1156
+ through: message.params?.through,
1157
+ maxBytes: message.params?.maxBytes,
1158
+ maxFrames: message.params?.maxFrames,
1159
+ });
1160
+ return JSON.stringify({
1161
+ jsonrpc: "2.0",
1162
+ id: message.id,
1163
+ result: { type: "output_journal", data },
1164
+ });
1165
+ }
1166
+ const journal = message.params?.outputRecovery === true
1167
+ ? readOutputJournal(sessionManager.getStorageDir(), sessionId)
1168
+ : undefined;
1169
+ const legacy = journal && journal.status !== "unavailable"
1170
+ ? readOutputJournalLegacyBase(sessionManager.getStorageDir(), sessionId, journal.legacyBaseThroughEventId)
1171
+ : undefined;
1172
+ const transcript = legacy
1173
+ ? legacy.events
1174
+ : readHubTranscript(sessionManager.getStorageDir(), sessionId);
1150
1175
  return JSON.stringify({
1151
1176
  jsonrpc: "2.0",
1152
1177
  id: message.id,
@@ -1154,6 +1179,7 @@ function replyToHostSessionQuery(message, sessionManager, workspaceCwd, isRunnin
1154
1179
  type: "session_detail",
1155
1180
  data: {
1156
1181
  state,
1182
+ ...(legacy ? { outputJournal: journal, legacyBaseComplete: legacy.complete } : {}),
1157
1183
  ...(runReplay ? runReplay.snapshot(sessionId, transcript) : { transcript }),
1158
1184
  running: isRunning(sessionId),
1159
1185
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,13 +60,13 @@
60
60
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
61
61
  },
62
62
  "dependencies": {
63
- "@cjhyy/code-shell-capability-coding": "0.9.26",
64
- "@cjhyy/code-shell-core": "0.9.26",
65
- "@cjhyy/code-shell-web": "0.9.26",
66
- "@modelcontextprotocol/sdk": "^1.12.1",
63
+ "@cjhyy/code-shell-capability-coding": "0.9.28",
64
+ "@cjhyy/code-shell-core": "0.9.28",
65
+ "@cjhyy/code-shell-web": "0.9.28",
66
+ "@modelcontextprotocol/sdk": "^1.31.0",
67
67
  "@types/ws": "^8.18.1",
68
68
  "ws": "^8.21.0",
69
- "@cjhyy/code-shell-link": "0.9.26"
69
+ "@cjhyy/code-shell-link": "0.9.28"
70
70
  },
71
71
  "engines": {
72
72
  "node": ">=20.10"