@cmmd-center/forge 0.13.11 → 0.13.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BunHttpServer-C7-1JJ7b.cjs → BunHttpServer-C0oPP_3A.cjs} +4 -4
- package/dist/{BunHttpServer-CVPGkizx.mjs → BunHttpServer-FbbQmsl_.mjs} +4 -4
- package/dist/{BunServices-CPrZSIfE.mjs → BunServices-DC7nhA3w.mjs} +2 -2
- package/dist/{BunServices-Dl-3897e.cjs → BunServices-DkenwD7S.cjs} +2 -2
- package/dist/{Cache-Cf3ARNaS.cjs → Cache-BmGc_2BZ.cjs} +2 -2
- package/dist/{Cache-C9cwdCDt.mjs → Cache-D57GWb9-.mjs} +2 -2
- package/dist/{HttpServer-CggZzScj.cjs → HttpServer-BBzRBmVt.cjs} +2 -2
- package/dist/{HttpServer-COaBeM7-.mjs → HttpServer-Bclwvu0I.mjs} +2 -2
- package/dist/{NodeHttpServer-UQnvZnmu.mjs → NodeHttpServer-BJ4l15f-.mjs} +5 -5
- package/dist/{NodeHttpServer-Bi339XFk.cjs → NodeHttpServer-Cjd3bFrD.cjs} +5 -5
- package/dist/{NodePTY-UOaRyQsj.cjs → NodePTY-BehazUMi.cjs} +2 -2
- package/dist/{NodePTY-B1u6RHXk.mjs → NodePTY-BhezYy9H.mjs} +2 -2
- package/dist/{NodeServices-R0BZsZ72.cjs → NodeServices-BvdIc9Vh.cjs} +1 -1
- package/dist/{NodeServices-CKFIZVu5.cjs → NodeServices-CtC4a-h5.cjs} +2 -2
- package/dist/{NodeServices-z1Ac4lEO.mjs → NodeServices-D3IxOzHl.mjs} +2 -2
- package/dist/{NodeSqliteClient-CyJvo9y9.cjs → NodeSqliteClient-CfsdC3AB.cjs} +4 -4
- package/dist/{NodeSqliteClient-BrzqdOsF.mjs → NodeSqliteClient-md8xd9hY.mjs} +4 -4
- package/dist/{NodeTerminal-pYVVRTiD.mjs → NodeTerminal-9iHZIdt1.mjs} +2 -2
- package/dist/{NodeTerminal-B89MobLp.cjs → NodeTerminal-CO-i-6Je.cjs} +2 -2
- package/dist/{Path-BunGrE8F.cjs → Path-BQOH_fWQ.cjs} +7 -1
- package/dist/{Path-evOm-H0Q.mjs → Path-CZJ1gPPo.mjs} +2 -2
- package/dist/{SqlError-CezTFGvr.mjs → SqlError-BMBkezUO.mjs} +2 -2
- package/dist/{SqlError-1wIMjXAf.cjs → SqlError-g4pLzNWR.cjs} +2 -2
- package/dist/{SqliteClient-D3Yd3ZHn.mjs → SqliteClient-BKB695RF.mjs} +3 -3
- package/dist/{SqliteClient-bo7iui1t.cjs → SqliteClient-O1ABIngw.cjs} +3 -3
- package/dist/bin.cjs +1451 -233
- package/dist/bin.mjs +1470 -260
- package/dist/client/assets/{CanvasPanel-z0F-jdf1.js → CanvasPanel-RpD9Eh_S.js} +2 -2
- package/dist/client/assets/{CompositeItem-P9UVXCKv.js → CompositeItem-DEIBUHJR.js} +2 -2
- package/dist/client/assets/{DiffPanel-BrlSm5kH.js → DiffPanel-cwm7LEtZ.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-OZhO8GfQ.js → DiffPanel.logic-B9AWYFxE.js} +2 -2
- package/dist/client/assets/{DiffPanelShell-DnugngHM.js → DiffPanelShell-BhokrrDZ.js} +112 -112
- package/dist/client/assets/{DiffWorkerPoolProvider-BOD7cQPe.js → DiffWorkerPoolProvider-BZjEmEzL.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-DACYpcbJ.js → PullRequestCodePanel-BPJINzCp.js} +2 -2
- package/dist/client/assets/{Virtualizer-R_LG4CvG.js → Virtualizer-i6HiQ7Qm.js} +2 -2
- package/dist/client/assets/{canvasStateStore-CdQxd2aO.js → canvasStateStore-NrAxDqvM.js} +2 -2
- package/dist/client/assets/cmmdAuthRedirect-BahlX0gF.js +2 -0
- package/dist/client/assets/{index-BXzYKPmD.js → index-DMWfw0Ih.js} +61 -61
- package/dist/client/assets/index-dEPiAWVG.css +1 -0
- package/dist/client/assets/{popover-CUSlb3eK.js → popover-Z8Tvl-OD.js} +2 -2
- package/dist/client/assets/src-BU7yHzO9.js +3 -0
- package/dist/client/assets/{toggle-BREV66nT.js → toggle-CWYdWYBn.js} +2 -2
- package/dist/client/index.html +11 -11
- package/dist/forge-build.json +1 -1
- package/dist/{open-D8mGyWmt.mjs → open-FIgTDosf.mjs} +3 -3
- package/dist/runtimeBootstrap.cjs +1 -1
- package/dist/runtimeBootstrap.mjs +1 -1
- package/dist/{runtimeControlAuthority-B3kNmYRT.cjs → runtimeControlAuthority-BkVB0uAA.cjs} +17 -1
- package/dist/{runtimeControlAuthority-D_zKr_eI.mjs → runtimeControlAuthority-E_kdeD_P.mjs} +12 -2
- package/package.json +1 -1
- package/dist/client/assets/cmmdAuthRedirect-Bp1uQaQ9.js +0 -2
- package/dist/client/assets/index-Bn-ASQRG.css +0 -1
- package/dist/client/assets/src-BL-mOkT1.js +0 -3
package/dist/bin.mjs
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-C5ckVeoz.mjs";
|
|
4
4
|
import { $ as encodeEffect, $a as make$39, $c as fromNullishOr, $i as spaced, $l as constTrue, $n as filterMapOrFail, $r as runPromise, $s as millis, $t as stringifyJson, Aa as Scope$1, Ai as void_$1, Al as format, An as cached$3, Ar as match$1, As as uninterruptibleMask, At as withConstructorDefault, Ba as CurrentLogSpans, Bc as add$2, Bi as counter, Bl as compose$1, Bn as catch_, Bo as addFinalizer, Br as orDie, Bs as make$42, Bt as head, C as Record, Ca as mergeAll, Ci as tx$1, Cn as andThen, Co as hasDies, Cr as logInfo, Cs as sleep$3, Ct as optionalKey, D as TaggedStruct, Da as succeedContext, Di as uninterruptibleMask$1, Dn as as$3, Do as interruptors, Ds as suspend, Dt as toCodecStringTree, E as TaggedErrorClass, Ea as succeed$2, Ei as uninterruptible, En as annotateSpans, Eo as interrupt$2, Er as map$3, Et as toCodecJson, F as UndefinedOr, Fa as forkUnsafe, Fc as isSuccess$1, Fi as withParentSpan, Fn as catchFilter, Fr as onError$1, Fs as MinimumTraceLevel, G as decodeExit, Ga as done, Gc as getUnsafe$1, Gi as withAttributes, Gl as isNullish, Gn as die$1, Gr as provideService, Gs as format$1, Gt as Transformation, H as declare, Hc as get$3, Hi as snapshotUnsafe, Hl as isNotNull, Hn as contextWith, Ho as as$2, Hr as promise, Ht as size$2, I as Union, Ia as make$37, Ii as withSpan, In as catchIf, Io as ConsoleRef, Ir as onExit, Is as NativeSpan, It as assign, J as decodeUnknownEffect, Ja as failCause$3, Jc as merge$2, Ji as exponential, Jn as exit, Jr as repeat, Js as hours, Jt as transformOrFail, K as decodeSync, Kc as make$35, Ki as addDelay, Kl as isObject$1, Ko as consolePretty$1, Kr as race, Ks as fromInput, L as Unknown, La as makeUnsafe$4, Lc as succeed$1, Li as withTracerEnabled, Ln as catchReason, Lo as CurrentLoggers$1, Ls as ParentSpan, Lt as append$1, M as URLFromString, Ma as addFinalizerExit, Mi as whileLoop, Mn as callback, Mo as pretty, Ms as void_$2, Na as close, Ni as withFiber$1, Nn as catchCause, No as prettyErrors, Nr as matchEffect, O as TemplateLiteral, Oa as sync$2, Ol as NodeInspectSymbol, On as asSome, Or as mapError, Os as sync, Ot as toJsonSchemaDocument, P as Undefined, Pa as closeUnsafe, Pc as isFailure$1, Pl as equals, Pn as catchDefect, Po as squash, Pr as never$1, Q as encode$4, Qc as flatten$1, Qi as recurs, Qn as filter$1, Qr as runForkWith, Qs as max, R as UnknownFromJsonString$1, Ra as provide$2, Rc as Reference$1, Ri as yieldNow, Rn as catchTag, Rr as onInterrupt, Rs as Tracer, Rt as drop$1, S as Option, Sa as merge$3, Sc as sort, Si as try_, Sn as all, Sr as logFatal, Ss as scopeAddFinalizerExit, St as optional$4, T as Struct, Ta as provideMerge, Ti as undefined_, Tl as withFiber, Tn as annotateLogs, To as hasInterruptsOnly, Tr as makeSpanScoped, Tt as tag, Ua as TracerTimingEnabled, Ui as timer, Ul as isNotNullish, Un as currentSpan, Ur as provide, Ut as toArray, V as brand, Va as MinimumLogLevel, Vc as empty$1, Vi as gauge, Vl as hasProperty, Vn as context, Vo as andThen$1, Vr as orElseSucceed, Vt as isEmpty$2, W as decodeEffect, Wa as _await, Wi as update$3, Wl as isNotUndefined, Wr as provideContext, Ws as days, X as decodeUnknownOption, Xc as firstSomeOf, Xl as cast, Xn as failCause$1, Xr as retry, Xt as onSome, Y as decodeUnknownExit, Yi as forever$2, Yn as fail, Yr as result, Z as decodeUnknownSync, Za as isDeferred, Zc as flatMap$2, Zi as passthrough$1, Zl as compose, Zr as runFork, Zt as parseJson$1, _ as NonEmptyArray, _a as effectContext, _i as tapErrorTag, _l as YieldableProto, _n as toUtc, _t as isSchemaError, a as DateTimeUtc, aa as Random$1, ac as toSeconds, al as isSome, an as value, ao as interrupt$3, ar as fnUntraced, at as isBetween, au as identity, b as NullOr, bi as timeoutOrElse, bn as acquireUseRelease, br as logDebug, bs as reportCauseUnsafe, bt as makeFilter, c as Defect, ca as Clock, cc as empty$3, ci as sleep$4, cl as none$4, cn as add$3, co as match$2, cr as forkChild, cs as flatMap$1, ct as isGreaterThanOrEqualTo, d as Exit, dc as flatMapNullishOr, di as succeedSome, dl as make$41, dn as isGreaterThanOrEqualTo$1, dr as forkScoped, ds as forkDetach, dt as isMaxLength, ea as catchDone, ec as min, el as fromUndefinedOr, er as filterOrFail, et as encodeSync, eu as constUndefined, fa as TaggedError, fc as fromIterable, fi as suspend$1, fn as make$38, fr as gen, ft as isMaxProperties, g as Never, ga as effect, gi as tapError, gl as PipeInspectableProto, gn as toEpochMillis, gr as interruptible, gt as isSchema, h as Literals, ha as buildWithMemoMap, hc as isReadonlyArrayNonEmpty, hi as tapCause, hn as toDate, hr as interrupt$1, ht as isPattern, i as Class, ic as toMillis, ii as scoped, il as isNone, in as make$40, io as failCause$2, ir as fn$1, it as is$1, ja as addFinalizer$1, jc as fail$1, jn as cachedWithTTL, jo as map$6, jr as matchCauseEffect, jt as withDecodingDefault, k as Trim, ka as unwrap, ki as useSpan, kl as toJson, kn as asVoid, ks as tracerLogger$1, kt as toType, l as Duration, la as currentTimeMillis, lc as ensure, li as succeed, ll as some, ln as distance, lr as forkDetach$1, ls as fnUntraced$1, lt as isInt, m as Literal, ma as build, mc as isArrayNonEmpty, mi as tap, mn as now, mo as StackTrace, mr as ignoreCause, ms as loggerMake, mt as isNonEmpty, n as ArraySchema, nc as seconds, nl as getOrNull, no as die$2, nt as encodeUnknownExit, nu as constant, o as DateTimeUtcFromString, oa as clamp, oc as zero, oi as service, ol as map$5, oo as isFailure, or as forEach, ot as isFinite$1, ou as pipe$1, p as Int$1, pa as CurrentMemoMap, pi as sync$1, pn as makeUnsafe$5, po as NoSuchElementError, pr as ignore, pt as isMinLength, q as decodeTo, qa as fail$3, qc as makeUnsafe$3, qi as both, ql as isTagged, qn as ensuring, qr as raceFirst, qs as fromInputUnsafe, r as Boolean$1, ra as isDoneCause, rc as times, ri as scope$1, rl as getOrUndefined, rn as makeFormatterDefault, ro as fail$2, rr as flatten$2, rt as fromJsonString, ru as dual, sa as parse$6, sc as allocate, si as serviceOption, sl as match, sn as encodeBase64Url$2, so as isSuccess, sr as forever$1, st as isGreaterThan, su as pipeArguments, t as Any, tc as minutes, ti as runPromiseWith, tl as getOrElse, tn as InvalidValue$1, to as succeed$3, tr as flatMap, ts as fiberInterrupt, tt as encodeUnknownEffect, tu as constVoid, u as ErrorClass, ua as Class$1, uc as filter, ui as succeedNone, ul as Number$2, un as formatIso, uo as void_$3, ur as forkIn, us as forever, ut as isLessThanOrEqualTo, v as NonEmptyString, va as effectDiscard, vi as timeout$1, vn as Transaction$1, vo as done$1, vr as isEffect, w as String$1, wa as provide$1, wi as txRetry, wl as isEffect$1, wn as annotateCurrentSpan, wo as hasInterrupts, wr as logWarning$1, wt as suspend$2, x as Number$1, xa as launch$1, xc as reverse, xi as tryPromise, xn as addFinalizer$2, xr as logError, xs as scope, xt as mutable, y as Null, ya as empty$4, yc as map$4, yi as timeoutOption, yn as acquireRelease, yo as fail$4, yr as log$4, yt as make$36, z as Void, za as CurrentLogAnnotations, zc as Service$1, zi as zipWith, zn as catchTags, zr as option, zs as externalSpan, zt as empty$2 } from "./Schema-DOBWF8gQ.mjs";
|
|
5
|
-
import { $ as
|
|
6
|
-
import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-
|
|
7
|
-
import { n as layer$7 } from "./NodeServices-
|
|
8
|
-
import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-
|
|
5
|
+
import { $ as takeWhile, A as fromSubscription, B as onExit$1, Bt as take$1, C as fromChannel, Ct as fromTransform, D as fromPull, Dt as makeUnsafe$6, E as fromPubSub, Et as make$44, F as mapEffect$1, Ft as offerAll, G as runCollect, Gt as subscribe$1, Ht as unbounded$2, I as mapError$1, It as offerUnsafe, J as runForEach, K as runDrain, Kt as unbounded$1, L as merge$4, M as make$43, N as map$7, Nt as make$45, O as fromQueue, Ot as bounded, P as mapAccum, Pt as offer$1, Qt as interrupt$4, Rt as shutdown$1, S as fromAsyncIterable, St as fromSubscriptionArray, T as fromIterable$1, U as provideContext$1, Ut as publish, Vt as takeAll, W as run$2, Wt as shutdown$2, Xt as await_, Y as runForEachArray, Yt as makeUnsafe$7, Z as succeed$4, Zt as getCurrent, _t as forEach$1, b as flatMap$3, bt as mapInput, c as callback$1, ct as make$46, d as debounce$1, en as runIn, et as tick, f as decodeText$1, g as ensuring$1, gt as drain, h as encodeText, j as isStream, jt as failCause$4, kt as end, l as catchCause$1, lt as get$5, m as empty$5, nt as toPull, ot as unwrap$1, q as runFold, r as FileSystem, st as get$4, t as Path$2, tt as toAsyncIterable, u as concat$2, ut as make$47, v as failCause$5, w as fromEffect, x as fromArray, y as filter$2, zt as sliding } from "./Path-CZJ1gPPo.mjs";
|
|
6
|
+
import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-9iHZIdt1.mjs";
|
|
7
|
+
import { n as layer$7 } from "./NodeServices-D3IxOzHl.mjs";
|
|
8
|
+
import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-Bclwvu0I.mjs";
|
|
9
9
|
import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-Cdfpphpo.mjs";
|
|
10
|
-
import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-
|
|
11
|
-
import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-
|
|
10
|
+
import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-D57GWb9-.mjs";
|
|
11
|
+
import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-BMBkezUO.mjs";
|
|
12
12
|
import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
|
|
13
13
|
import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
|
|
14
|
-
import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as
|
|
14
|
+
import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as runtimeGrantToken, f as setRuntimeGrantToken, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, p as createReadOnceFileTokenAccessor, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as verifyCmmdScopedRuntimePayload } from "./runtimeControlAuthority-E_kdeD_P.mjs";
|
|
15
15
|
import { n as PtySpawnError, t as PtyAdapter } from "./PTY-BzdbZ0Ym.mjs";
|
|
16
16
|
import { createRequire } from "node:module";
|
|
17
17
|
import * as HE from "node:child_process";
|
|
@@ -22,7 +22,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
|
|
|
22
22
|
import * as FS$1 from "node:fs";
|
|
23
23
|
import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
24
|
import * as NodeOS from "node:os";
|
|
25
|
-
import
|
|
25
|
+
import os, { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
26
26
|
import * as Path$1 from "node:path";
|
|
27
27
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
28
28
|
import { pathToFileURL } from "node:url";
|
|
@@ -65,12 +65,12 @@ import * as tls from "node:tls";
|
|
|
65
65
|
import { performance as performance$1 } from "perf_hooks";
|
|
66
66
|
import { execFile as execFile$1, spawn as spawn$1 } from "child_process";
|
|
67
67
|
import * as NodeFSP from "node:fs/promises";
|
|
68
|
-
import fs$1, { link, lstat, mkdir, mkdtemp, readFile as readFile$1, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
68
|
+
import fs$1, { chmod, link, lstat, mkdir, mkdtemp, readFile as readFile$1, readdir as readdir$1, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
69
69
|
import { StringDecoder } from "string_decoder";
|
|
70
70
|
import * as timers from "node:timers/promises";
|
|
71
71
|
import { pipeline } from "node:stream/promises";
|
|
72
72
|
import z$ from "http2";
|
|
73
|
-
import { access, appendFile, chmod, copyFile, link as link$1, lstat as lstat$1, mkdir as mkdir$1, open, readFile as readFile$2, readdir as readdir$
|
|
73
|
+
import { access, appendFile, chmod as chmod$1, copyFile, link as link$1, lstat as lstat$1, mkdir as mkdir$1, open, readFile as readFile$2, readdir as readdir$2, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, rmdir, stat as stat$1, symlink, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
|
|
74
74
|
import { createInterface as createInterface$1 } from "readline";
|
|
75
75
|
import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
|
|
76
76
|
import { chromium } from "playwright";
|
|
@@ -17358,7 +17358,8 @@ const ORCHESTRATION_WS_METHODS = {
|
|
|
17358
17358
|
getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
|
|
17359
17359
|
subscribeShell: "orchestration.subscribeShell",
|
|
17360
17360
|
subscribeThread: "orchestration.subscribeThread",
|
|
17361
|
-
searchThreads: "orchestration.searchThreads"
|
|
17361
|
+
searchThreads: "orchestration.searchThreads",
|
|
17362
|
+
getThreadDetailPage: "orchestration.getThreadDetailPage"
|
|
17362
17363
|
};
|
|
17363
17364
|
const ProviderKind = Literals([
|
|
17364
17365
|
"codex",
|
|
@@ -17868,10 +17869,39 @@ const OrchestrationShellStreamItem = Union([Struct({
|
|
|
17868
17869
|
kind: Literal("snapshot"),
|
|
17869
17870
|
snapshot: OrchestrationShellSnapshot
|
|
17870
17871
|
}), OrchestrationShellStreamEvent]);
|
|
17871
|
-
const OrchestrationSubscribeThreadInput = Struct({
|
|
17872
|
+
const OrchestrationSubscribeThreadInput = Struct({
|
|
17873
|
+
threadId: ThreadId,
|
|
17874
|
+
afterSequence: optionalKey(NonNegativeInt),
|
|
17875
|
+
requestCompletionMarker: optionalKey(Boolean$1),
|
|
17876
|
+
turnLimit: optionalKey(PositiveInt)
|
|
17877
|
+
});
|
|
17878
|
+
Struct({
|
|
17879
|
+
turnLimit: optionalKey(PositiveInt),
|
|
17880
|
+
beforeCursor: optionalKey(TrimmedNonEmptyString)
|
|
17881
|
+
});
|
|
17882
|
+
/**
|
|
17883
|
+
* Reads one windowed page of a thread's detail directly over the RPC socket.
|
|
17884
|
+
*
|
|
17885
|
+
* The HTTP snapshot route and `subscribeThread`'s fallback snapshot both take
|
|
17886
|
+
* the same `turnLimit`/`beforeCursor` window, but neither can serve "load
|
|
17887
|
+
* earlier" on a connection with no HTTP endpoint: the fallback snapshot only
|
|
17888
|
+
* fires once, on subscribe. This method exists for exactly that case (a
|
|
17889
|
+
* relay-only connection), gated on the server's `threadDetailPageRpc`
|
|
17890
|
+
* capability so older servers are never sent a method they do not know.
|
|
17891
|
+
*/
|
|
17892
|
+
const OrchestrationGetThreadDetailPageInput = Struct({
|
|
17893
|
+
threadId: ThreadId,
|
|
17894
|
+
turnLimit: optionalKey(PositiveInt),
|
|
17895
|
+
beforeCursor: optionalKey(TrimmedNonEmptyString)
|
|
17896
|
+
});
|
|
17872
17897
|
const OrchestrationThreadDetailSnapshot = Struct({
|
|
17873
17898
|
snapshotSequence: NonNegativeInt,
|
|
17874
|
-
thread: OrchestrationThread
|
|
17899
|
+
thread: OrchestrationThread,
|
|
17900
|
+
page: optional$4(Struct({
|
|
17901
|
+
beforeCursor: NullOr(TrimmedNonEmptyString),
|
|
17902
|
+
hasMore: Boolean$1,
|
|
17903
|
+
snapshotSequence: NonNegativeInt
|
|
17904
|
+
}))
|
|
17875
17905
|
});
|
|
17876
17906
|
const ProjectCreateCommand = Struct({
|
|
17877
17907
|
type: Literal("project.create"),
|
|
@@ -18675,13 +18705,17 @@ const OrchestrationEvent = Union([
|
|
|
18675
18705
|
payload: ThreadRateLimitSetPayload$1
|
|
18676
18706
|
})
|
|
18677
18707
|
]);
|
|
18678
|
-
const OrchestrationThreadStreamItem = Union([
|
|
18679
|
-
kind: Literal("
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18684
|
-
|
|
18708
|
+
const OrchestrationThreadStreamItem = Union([
|
|
18709
|
+
Struct({ kind: Literal("synchronized") }),
|
|
18710
|
+
Struct({
|
|
18711
|
+
kind: Literal("snapshot"),
|
|
18712
|
+
snapshot: OrchestrationThreadDetailSnapshot
|
|
18713
|
+
}),
|
|
18714
|
+
Struct({
|
|
18715
|
+
kind: Literal("event"),
|
|
18716
|
+
event: OrchestrationEvent
|
|
18717
|
+
})
|
|
18718
|
+
]);
|
|
18685
18719
|
const OrchestrationCommandReceiptStatus = Literals(["accepted", "rejected"]);
|
|
18686
18720
|
const TurnCountRange = Struct({
|
|
18687
18721
|
fromTurnCount: NonNegativeInt,
|
|
@@ -18824,6 +18858,10 @@ const OrchestrationRpcSchemas = {
|
|
|
18824
18858
|
searchThreads: {
|
|
18825
18859
|
input: OrchestrationSearchThreadsInput,
|
|
18826
18860
|
output: OrchestrationSearchThreadsResult
|
|
18861
|
+
},
|
|
18862
|
+
getThreadDetailPage: {
|
|
18863
|
+
input: OrchestrationGetThreadDetailPageInput,
|
|
18864
|
+
output: OrchestrationThreadDetailSnapshot
|
|
18827
18865
|
}
|
|
18828
18866
|
};
|
|
18829
18867
|
var OrchestrationGetSnapshotError = class extends TaggedErrorClass()("OrchestrationGetSnapshotError", {
|
|
@@ -22792,6 +22830,9 @@ const ServerConfig$1 = Struct({
|
|
|
22792
22830
|
availableEditors: ArraySchema(EditorId),
|
|
22793
22831
|
shellRevealInFileManager: optionalKey(Boolean$1),
|
|
22794
22832
|
shellRevealInFileManagerKind: optionalKey(FileManagerRevealKind),
|
|
22833
|
+
threadResumeCompletionMarker: optionalKey(Boolean$1),
|
|
22834
|
+
threadSnapshotPagination: optionalKey(Boolean$1),
|
|
22835
|
+
threadDetailPageRpc: optionalKey(Boolean$1),
|
|
22795
22836
|
observability: ServerObservability,
|
|
22796
22837
|
settings: ServerSettings
|
|
22797
22838
|
});
|
|
@@ -26658,6 +26699,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
|
|
|
26658
26699
|
error: OrchestrationSearchThreadsError
|
|
26659
26700
|
});
|
|
26660
26701
|
/**
|
|
26702
|
+
* The socket-only path for "load earlier turns": a relay-only connection has
|
|
26703
|
+
* no HTTP endpoint, so this reads the same windowed page the HTTP snapshot
|
|
26704
|
+
* route and `subscribeThread`'s fallback snapshot serve, over the RPC socket
|
|
26705
|
+
* instead.
|
|
26706
|
+
*/
|
|
26707
|
+
const WsOrchestrationGetThreadDetailPageRpc = make$30(ORCHESTRATION_WS_METHODS.getThreadDetailPage, {
|
|
26708
|
+
payload: OrchestrationRpcSchemas.getThreadDetailPage.input,
|
|
26709
|
+
success: OrchestrationRpcSchemas.getThreadDetailPage.output,
|
|
26710
|
+
error: OrchestrationGetSnapshotError
|
|
26711
|
+
});
|
|
26712
|
+
/**
|
|
26661
26713
|
* Resolve `owner/repo` to the repository the caller is allowed to clone.
|
|
26662
26714
|
*
|
|
26663
26715
|
* Both of these run against the same CMMD-brokered GitHub credential the web
|
|
@@ -26770,7 +26822,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
|
|
|
26770
26822
|
payload: ProviderUploadFeedbackInput,
|
|
26771
26823
|
success: ProviderUploadFeedbackResult,
|
|
26772
26824
|
error: ProviderUploadFeedbackError
|
|
26773
|
-
}), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
|
|
26825
|
+
}), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
|
|
26774
26826
|
const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
|
|
26775
26827
|
/**
|
|
26776
26828
|
* The direct-WebSocket mount of the same group, with per-method scope
|
|
@@ -26843,6 +26895,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
|
|
|
26843
26895
|
}
|
|
26844
26896
|
};
|
|
26845
26897
|
}
|
|
26898
|
+
const WORKTREE_LAUNCH_ACTIVITY_KINDS = new Set([
|
|
26899
|
+
"worktree.requested",
|
|
26900
|
+
"worktree.stage",
|
|
26901
|
+
"worktree.output",
|
|
26902
|
+
"worktree.created",
|
|
26903
|
+
"worktree.failed",
|
|
26904
|
+
"setup-script.requested",
|
|
26905
|
+
"setup-script.started",
|
|
26906
|
+
"setup-script.skipped",
|
|
26907
|
+
"setup-script.failed",
|
|
26908
|
+
"launch.agent-starting",
|
|
26909
|
+
"launch.cancelled"
|
|
26910
|
+
]);
|
|
26911
|
+
function encodeCmmdRuntimeWorktreeLaunchActivityPayload(kind, payload) {
|
|
26912
|
+
return {
|
|
26913
|
+
...isRecord$23(payload) ? payload : { sourcePayload: payload },
|
|
26914
|
+
forgeWorktreeLaunchActivity: {
|
|
26915
|
+
schemaVersion: 1,
|
|
26916
|
+
source: "native",
|
|
26917
|
+
kind
|
|
26918
|
+
}
|
|
26919
|
+
};
|
|
26920
|
+
}
|
|
26921
|
+
function isCmmdRuntimeWorktreeLaunchActivityKind(kind) {
|
|
26922
|
+
return WORKTREE_LAUNCH_ACTIVITY_KINDS.has(kind);
|
|
26923
|
+
}
|
|
26846
26924
|
//#endregion
|
|
26847
26925
|
//#region ../../packages/contracts/src/cmmdProjectPatch.ts
|
|
26848
26926
|
const CMMD_PROJECT_PATCH_FIELDS = {
|
|
@@ -31945,6 +32023,11 @@ const RELAY_METHOD_POLICY_BASES = [
|
|
|
31945
32023
|
requiredScope: "thread.read",
|
|
31946
32024
|
deliveryClass: "subscription"
|
|
31947
32025
|
},
|
|
32026
|
+
{
|
|
32027
|
+
method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
|
|
32028
|
+
requiredScope: "thread.read",
|
|
32029
|
+
deliveryClass: "read"
|
|
32030
|
+
},
|
|
31948
32031
|
{
|
|
31949
32032
|
method: WS_METHODS.assetsCreateUrl,
|
|
31950
32033
|
requiredScope: "thread.read",
|
|
@@ -50614,7 +50697,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
50614
50697
|
const min = forceColor || 0;
|
|
50615
50698
|
if (env.TERM === "dumb") return min;
|
|
50616
50699
|
if (process$1.platform === "win32") {
|
|
50617
|
-
const osRelease =
|
|
50700
|
+
const osRelease = os.release().split(".");
|
|
50618
50701
|
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
50619
50702
|
return 1;
|
|
50620
50703
|
}
|
|
@@ -53057,7 +53140,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
53057
53140
|
exports.processDetector = void 0;
|
|
53058
53141
|
const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
|
|
53059
53142
|
const semconv_1 = require_semconv$1();
|
|
53060
|
-
const os$
|
|
53143
|
+
const os$4 = __require("os");
|
|
53061
53144
|
/**
|
|
53062
53145
|
* ProcessDetector will be used to detect the resources related current process running
|
|
53063
53146
|
* and being instrumented from the NodeJS Process module.
|
|
@@ -53079,7 +53162,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
53079
53162
|
};
|
|
53080
53163
|
if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
|
|
53081
53164
|
try {
|
|
53082
|
-
const userInfo = os$
|
|
53165
|
+
const userInfo = os$4.userInfo();
|
|
53083
53166
|
attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
|
|
53084
53167
|
} catch (e) {
|
|
53085
53168
|
api_1.diag.debug(`error obtaining process owner: ${e}`);
|
|
@@ -66002,7 +66085,7 @@ function normalizeNumberish(value) {
|
|
|
66002
66085
|
}
|
|
66003
66086
|
//#endregion
|
|
66004
66087
|
//#region package.json
|
|
66005
|
-
var version$1 = "0.13.
|
|
66088
|
+
var version$1 = "0.13.13";
|
|
66006
66089
|
//#endregion
|
|
66007
66090
|
//#region src/sentry.ts
|
|
66008
66091
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -66404,6 +66487,129 @@ const respondToAuthError = (error) => gen(function* () {
|
|
|
66404
66487
|
}));
|
|
66405
66488
|
return response;
|
|
66406
66489
|
});
|
|
66490
|
+
//#endregion
|
|
66491
|
+
//#region src/http/rateLimit.ts
|
|
66492
|
+
/**
|
|
66493
|
+
* Per-client, per-route-class rate limiting for Forge's own HTTP surface.
|
|
66494
|
+
*
|
|
66495
|
+
* Scoped to the routes a broken or looping client can hammer before it ever
|
|
66496
|
+
* has an authenticated session — the CMMD device-code poll, the bootstrap and
|
|
66497
|
+
* pairing-token exchanges — plus the authenticated /api/cmmd proxy, which
|
|
66498
|
+
* every desktop UI interaction runs through. None of these had any limit
|
|
66499
|
+
* before FOR-36706: a stuck pairing loop or a poll that never saw `slow_down`
|
|
66500
|
+
* could retry as fast as the network allowed, with nothing to stop it and
|
|
66501
|
+
* nothing telling the client to back off.
|
|
66502
|
+
*
|
|
66503
|
+
* Defaults are generous on purpose. This exists to bound a runaway client,
|
|
66504
|
+
* not to throttle a working one — see `defaultRateLimitPolicies` for the
|
|
66505
|
+
* reasoning behind each number and how to override it.
|
|
66506
|
+
*
|
|
66507
|
+
* Deliberately plain module state, not an Effect service: every route this
|
|
66508
|
+
* guards is a synchronous decision (count, compare, maybe reset a window),
|
|
66509
|
+
* and threading a new Context requirement through the whole server's layer
|
|
66510
|
+
* graph for that would touch every test file that stands up any of these
|
|
66511
|
+
* routes. `cmmdJwtResolver.ts`'s in-flight-refresh map is the same call.
|
|
66512
|
+
*/
|
|
66513
|
+
function envInt(name, fallback) {
|
|
66514
|
+
const raw = process.env[name];
|
|
66515
|
+
if (!raw) return fallback;
|
|
66516
|
+
const parsed = Number(raw);
|
|
66517
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
66518
|
+
}
|
|
66519
|
+
/**
|
|
66520
|
+
* One reasoning line per class, so a future change to a default has to
|
|
66521
|
+
* confront why the old one was picked:
|
|
66522
|
+
*
|
|
66523
|
+
* - `auth-device-poll` (`/api/auth/cmmd-oauth/device` and `/token`): RFC 8628
|
|
66524
|
+
* polls every `interval` seconds, 5 by default, so a well-behaved client
|
|
66525
|
+
* sits under 12/min. 120/min leaves room for several concurrent sign-in
|
|
66526
|
+
* attempts and CMMD's own `slow_down` handling without ever tripping on
|
|
66527
|
+
* normal use, while still bounding a client that ignores `slow_down`.
|
|
66528
|
+
* - `auth-bootstrap` (`/api/auth/bootstrap*`, the bearer bootstrap routes):
|
|
66529
|
+
* a working client bootstraps once per sign-in. 30/min is generous headroom
|
|
66530
|
+
* for retries after a network blip, not a budget anyone should approach.
|
|
66531
|
+
* - `auth-token-exchange` (`/oauth/token`, the vendored pairing exchange):
|
|
66532
|
+
* same shape as bootstrap — one pairing attempt, occasionally retried.
|
|
66533
|
+
* - `cmmd-proxy` (every `/api/cmmd/*` route): this is the one real desktop
|
|
66534
|
+
* traffic actually leans on — task lists, project browsing, comments, all
|
|
66535
|
+
* day. 1200/min (20/sec) is set high enough that no normal session should
|
|
66536
|
+
* ever see it; it exists for a retry loop or a runaway resync, not for
|
|
66537
|
+
* ordinary use.
|
|
66538
|
+
*/
|
|
66539
|
+
function buildPoliciesFromEnv() {
|
|
66540
|
+
return {
|
|
66541
|
+
"auth-device-poll": {
|
|
66542
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_MAX", 120),
|
|
66543
|
+
windowMs: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_WINDOW_SECONDS", 60) * 1e3
|
|
66544
|
+
},
|
|
66545
|
+
"auth-bootstrap": {
|
|
66546
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_MAX", 30),
|
|
66547
|
+
windowMs: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_WINDOW_SECONDS", 60) * 1e3
|
|
66548
|
+
},
|
|
66549
|
+
"auth-token-exchange": {
|
|
66550
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_MAX", 30),
|
|
66551
|
+
windowMs: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_WINDOW_SECONDS", 60) * 1e3
|
|
66552
|
+
},
|
|
66553
|
+
"cmmd-proxy": {
|
|
66554
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_MAX", 1200),
|
|
66555
|
+
windowMs: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_WINDOW_SECONDS", 60) * 1e3
|
|
66556
|
+
}
|
|
66557
|
+
};
|
|
66558
|
+
}
|
|
66559
|
+
let policies = buildPoliciesFromEnv();
|
|
66560
|
+
const windows = /* @__PURE__ */ new Map();
|
|
66561
|
+
function windowKey(routeClass, clientKey) {
|
|
66562
|
+
return `${routeClass}\0${clientKey}`;
|
|
66563
|
+
}
|
|
66564
|
+
/**
|
|
66565
|
+
* Fixed-window counting: cheap, and precise enough for a loop-guard rather
|
|
66566
|
+
* than a security boundary. The one imprecision this trades away — up to 2x
|
|
66567
|
+
* the configured rate right at a window boundary — matters for an attacker
|
|
66568
|
+
* budgeting requests against the limiter's clock, not for the runaway
|
|
66569
|
+
* clients this exists to catch.
|
|
66570
|
+
*/
|
|
66571
|
+
function checkHttpRateLimit(input) {
|
|
66572
|
+
const policy = policies[input.routeClass];
|
|
66573
|
+
const nowMs = Date.now();
|
|
66574
|
+
const key = windowKey(input.routeClass, input.clientKey);
|
|
66575
|
+
const existing = windows.get(key);
|
|
66576
|
+
const windowIsLive = existing !== void 0 && nowMs - existing.windowStartMs < policy.windowMs;
|
|
66577
|
+
const windowStartMs = windowIsLive ? existing.windowStartMs : nowMs;
|
|
66578
|
+
const count = (windowIsLive ? existing.count : 0) + 1;
|
|
66579
|
+
windows.set(key, {
|
|
66580
|
+
windowStartMs,
|
|
66581
|
+
count
|
|
66582
|
+
});
|
|
66583
|
+
return {
|
|
66584
|
+
allowed: count <= policy.maxRequests,
|
|
66585
|
+
retryAfterSeconds: Math.max(0, Math.ceil((windowStartMs + policy.windowMs - nowMs) / 1e3))
|
|
66586
|
+
};
|
|
66587
|
+
}
|
|
66588
|
+
/**
|
|
66589
|
+
* Checks the limiter for one request and, when the caller is over budget,
|
|
66590
|
+
* returns the 429 to send instead of running the route — `null` means
|
|
66591
|
+
* proceed. Every route this guards is expected to call this first and return
|
|
66592
|
+
* early on a non-null result, before doing any other work.
|
|
66593
|
+
*/
|
|
66594
|
+
function rateLimitOrRespond(input) {
|
|
66595
|
+
const decision = checkHttpRateLimit(input);
|
|
66596
|
+
if (decision.allowed) return null;
|
|
66597
|
+
return jsonUnsafe({ error: "Too many requests. Try again later." }, {
|
|
66598
|
+
status: 429,
|
|
66599
|
+
headers: { "retry-after": String(decision.retryAfterSeconds) }
|
|
66600
|
+
});
|
|
66601
|
+
}
|
|
66602
|
+
/**
|
|
66603
|
+
* The client identity for a route with no session yet — the device-code
|
|
66604
|
+
* poll, bootstrap and pairing-token exchange all run before Forge has issued
|
|
66605
|
+
* one. IP address is the only stable-enough signal available at that point;
|
|
66606
|
+
* the authenticated `/api/cmmd` proxy keys on the session instead (see its
|
|
66607
|
+
* call site), so one desktop and one phone sharing a NAT never share a
|
|
66608
|
+
* budget there.
|
|
66609
|
+
*/
|
|
66610
|
+
function clientKeyFromRequest(request) {
|
|
66611
|
+
return normalizeIpAddress(getOrNull(request.remoteAddress)) ?? "unknown";
|
|
66612
|
+
}
|
|
66407
66613
|
const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
66408
66614
|
/**
|
|
66409
66615
|
* The one exchange this endpoint performs, or the reason it will not.
|
|
@@ -66429,6 +66635,29 @@ function accessTokenExpiresInSeconds(remainingMillis) {
|
|
|
66429
66635
|
return Math.max(0, Math.floor(remainingMillis / 1e3));
|
|
66430
66636
|
}
|
|
66431
66637
|
//#endregion
|
|
66638
|
+
//#region src/auth/commandBootstrapClient.ts
|
|
66639
|
+
const BOOTSTRAP_CLIENT_LABELS = {
|
|
66640
|
+
desktop: "Forge Desktop Cloud",
|
|
66641
|
+
mobile: "Forge Mobile Cloud"
|
|
66642
|
+
};
|
|
66643
|
+
/**
|
|
66644
|
+
* Client metadata for a bearer bootstrap session.
|
|
66645
|
+
*
|
|
66646
|
+
* The declared device type wins over the one inferred from the user agent. A
|
|
66647
|
+
* React Native fetch sends whatever the platform's networking stack sends, and
|
|
66648
|
+
* on iOS that is a CFNetwork string no user-agent heuristic reads as a phone.
|
|
66649
|
+
*/
|
|
66650
|
+
function resolveCommandBootstrapClient(input) {
|
|
66651
|
+
const deviceType = input.clientDeviceType ?? "desktop";
|
|
66652
|
+
return {
|
|
66653
|
+
...deriveAuthClientMetadata({
|
|
66654
|
+
request: input.request,
|
|
66655
|
+
label: BOOTSTRAP_CLIENT_LABELS[deviceType]
|
|
66656
|
+
}),
|
|
66657
|
+
deviceType
|
|
66658
|
+
};
|
|
66659
|
+
}
|
|
66660
|
+
//#endregion
|
|
66432
66661
|
//#region ../../packages/shared/src/cmmdIdentity.ts
|
|
66433
66662
|
const MAX_CMMD_AVATAR_URL_LENGTH = 256 * 1024;
|
|
66434
66663
|
const DATA_IMAGE_AVATAR_PATTERN = /^data:image\/(?:avif|gif|jpeg|jpg|png|webp);base64,[a-z0-9+/=]+$/i;
|
|
@@ -66882,14 +67111,26 @@ function extractJwtExpiresAt(jwtToken) {
|
|
|
66882
67111
|
}
|
|
66883
67112
|
}
|
|
66884
67113
|
/**
|
|
66885
|
-
*
|
|
66886
|
-
*
|
|
67114
|
+
* How long before `exp` a stored JWT is already treated as spent.
|
|
67115
|
+
*
|
|
67116
|
+
* Comparing against `exp` exactly leaves a window where Forge sends a token it
|
|
67117
|
+
* believes is good and CMMD has already stopped accepting it: the clocks differ by a
|
|
67118
|
+
* little, the request spends time in flight, and the answer comes back 401 on a
|
|
67119
|
+
* session that had a perfectly good refresh token sitting next to it. Two
|
|
67120
|
+
* minutes is far inside the JWT's own lifetime, so this costs one early
|
|
67121
|
+
* rotation and removes the race.
|
|
67122
|
+
*/
|
|
67123
|
+
const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
|
|
67124
|
+
/**
|
|
67125
|
+
* Returns true when a stored JWT is at or near its expiry, or has no decodable
|
|
67126
|
+
* expiry. CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is
|
|
67127
|
+
* not safe.
|
|
66887
67128
|
*/
|
|
66888
67129
|
function isJwtExpiredForSession(sessionId) {
|
|
66889
67130
|
if (!jwtCache.has(sessionId)) return false;
|
|
66890
67131
|
const expiresAt = jwtExpiryCache.get(sessionId);
|
|
66891
67132
|
if (!expiresAt) return true;
|
|
66892
|
-
return new Date(expiresAt)
|
|
67133
|
+
return new Date(expiresAt).getTime() - JWT_EXPIRY_SKEW_MS <= Date.now();
|
|
66893
67134
|
}
|
|
66894
67135
|
/**
|
|
66895
67136
|
* Hydrate the in-memory cache from the configured persistence layer. Call once at server startup
|
|
@@ -67445,10 +67686,27 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId) => gen(function* () {
|
|
|
67445
67686
|
* Retire refresh credentials copied into historical desktop bearer sessions.
|
|
67446
67687
|
* Electron owns rotation for native desktop OAuth; Forge Local keeps only the
|
|
67447
67688
|
* derived access authority needed by the current bearer session.
|
|
67448
|
-
|
|
67449
|
-
|
|
67450
|
-
|
|
67451
|
-
|
|
67689
|
+
*
|
|
67690
|
+
* Scoped to the bootstrapping client, not to the whole subject. The phone and
|
|
67691
|
+
* Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
|
|
67692
|
+
* desktop launch null out a phone's refresh token and end its session a day
|
|
67693
|
+
* later. Two clauses keep that from happening:
|
|
67694
|
+
*
|
|
67695
|
+
* - `client_device_type = 'desktop'`, which now excludes phones outright,
|
|
67696
|
+
* because the mobile app declares itself at bootstrap.
|
|
67697
|
+
* - the same recorded `client_user_agent` as the bootstrap doing the
|
|
67698
|
+
* retirement, which is the only thing that separates a phone row written
|
|
67699
|
+
* before that stamp existed from a real Electron row. Both are stamped
|
|
67700
|
+
* `desktop`; neither carries any other mark. An empty or unmatched user agent
|
|
67701
|
+
* retires nothing, which is the direction that keeps a live credential.
|
|
67702
|
+
*
|
|
67703
|
+
* Electron re-bootstraps under a stable user agent, so its own historical rows
|
|
67704
|
+
* still match and still get swept.
|
|
67705
|
+
*/
|
|
67706
|
+
const clearDesktopBearerRefreshTokensForSubject = (input) => gen(function* () {
|
|
67707
|
+
const normalizedSubject = input.subject.trim();
|
|
67708
|
+
const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
|
|
67709
|
+
if (!normalizedSubject || !normalizedUserAgent) return;
|
|
67452
67710
|
const rows = yield* (yield* SqlClient)`
|
|
67453
67711
|
UPDATE command_access_tokens
|
|
67454
67712
|
SET
|
|
@@ -67461,6 +67719,7 @@ const clearDesktopBearerRefreshTokensForSubject = (subject) => gen(function* ()
|
|
|
67461
67719
|
AND role = 'owner'
|
|
67462
67720
|
AND method = 'bearer-session-token'
|
|
67463
67721
|
AND client_device_type = 'desktop'
|
|
67722
|
+
AND client_user_agent = ${normalizedUserAgent}
|
|
67464
67723
|
)
|
|
67465
67724
|
RETURNING session_id AS "sessionId"
|
|
67466
67725
|
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
|
|
@@ -67483,6 +67742,17 @@ const OAUTH_STATE_COOKIE = "forge_oauth_state";
|
|
|
67483
67742
|
const OAUTH_VERIFIER_COOKIE = "forge_oauth_verifier";
|
|
67484
67743
|
//#endregion
|
|
67485
67744
|
//#region src/auth/http/cloudOwnerOAuth.ts
|
|
67745
|
+
const FORGE_CLOUD_SESSION_GRANT_COOKIE = "forge_cloud_session_grant";
|
|
67746
|
+
function resolveCloudOwnerSessionGrantCookie(input) {
|
|
67747
|
+
const token = input.token?.trim();
|
|
67748
|
+
if (!token || !input.expiresAt) return null;
|
|
67749
|
+
const expiresAt = new Date(input.expiresAt);
|
|
67750
|
+
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= Date.now()) return null;
|
|
67751
|
+
return {
|
|
67752
|
+
token,
|
|
67753
|
+
expiresAt
|
|
67754
|
+
};
|
|
67755
|
+
}
|
|
67486
67756
|
function validateCloudOwnerCallbackAssertion(input) {
|
|
67487
67757
|
if (input.serverAuth.isForgeCloudShell?.() !== true) return succeed(null);
|
|
67488
67758
|
if (!input.assertionToken || !input.serverAuth.verifyForgeCloudOwnerAssertion) return fail(new AuthError({
|
|
@@ -67546,8 +67816,19 @@ function cloudOwnerAssertionCookieOptions(assertion) {
|
|
|
67546
67816
|
secure: true
|
|
67547
67817
|
};
|
|
67548
67818
|
}
|
|
67549
|
-
function
|
|
67550
|
-
return
|
|
67819
|
+
function cloudOwnerSessionGrantCookieOptions(grant) {
|
|
67820
|
+
return {
|
|
67821
|
+
expires: grant.expiresAt,
|
|
67822
|
+
httpOnly: true,
|
|
67823
|
+
path: "/api/auth",
|
|
67824
|
+
sameSite: "lax",
|
|
67825
|
+
secure: true
|
|
67826
|
+
};
|
|
67827
|
+
}
|
|
67828
|
+
function cloudOwnerOAuthResponse(homeUrl, assertion, sessionGrant) {
|
|
67829
|
+
let response = redirect(homeUrl, { status: 302 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
|
|
67830
|
+
if (sessionGrant) response = response.pipe(flatMap(setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
|
|
67831
|
+
return response.pipe(flatMap(setCookie(OAUTH_STATE_COOKIE, "", expiredCallbackCookie)), flatMap(setCookie(OAUTH_VERIFIER_COOKIE, "", expiredCallbackCookie)));
|
|
67551
67832
|
}
|
|
67552
67833
|
//#endregion
|
|
67553
67834
|
//#region src/auth/http.ts
|
|
@@ -67597,6 +67878,56 @@ const authSessionRouteLayer = add$1("GET", "/api/auth/session", gen(function* ()
|
|
|
67597
67878
|
}));
|
|
67598
67879
|
return response;
|
|
67599
67880
|
}));
|
|
67881
|
+
const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", gen(function* () {
|
|
67882
|
+
const request = yield* HttpServerRequest;
|
|
67883
|
+
const serverAuth = yield* ServerAuth;
|
|
67884
|
+
if (serverAuth.isForgeCloudShell?.() !== true) return yield* new AuthError({
|
|
67885
|
+
message: "Session renewal is unavailable.",
|
|
67886
|
+
status: 401
|
|
67887
|
+
});
|
|
67888
|
+
const sessionGrant = request.cookies[FORGE_CLOUD_SESSION_GRANT_COOKIE]?.trim();
|
|
67889
|
+
if (!sessionGrant) return yield* new AuthError({
|
|
67890
|
+
message: "Session renewal is unavailable.",
|
|
67891
|
+
status: 401
|
|
67892
|
+
});
|
|
67893
|
+
const credentials = yield* resolveCmmdTokenBundleCredentials(yield* readCmmdJsonResponse(yield* tryPromise({
|
|
67894
|
+
try: () => fetch(resolveCommandAppEndpoint("/api/mcp-server/oauth/token"), {
|
|
67895
|
+
method: "POST",
|
|
67896
|
+
headers: { "content-type": "application/json" },
|
|
67897
|
+
body: JSON.stringify({
|
|
67898
|
+
grant_type: "forge_session_grant",
|
|
67899
|
+
grant_token: sessionGrant,
|
|
67900
|
+
client_id: "cmmd-forge-web"
|
|
67901
|
+
})
|
|
67902
|
+
}),
|
|
67903
|
+
catch: (cause) => new AuthError({
|
|
67904
|
+
message: "Failed to renew the Forge Cloud session with CMMD.",
|
|
67905
|
+
status: 500,
|
|
67906
|
+
cause
|
|
67907
|
+
})
|
|
67908
|
+
}), "Failed to renew the Forge Cloud session with CMMD."));
|
|
67909
|
+
const { cmmdIdentity, userId, patToStore } = credentials;
|
|
67910
|
+
if (!cmmdIdentity || !userId || !patToStore) return yield* new AuthError({
|
|
67911
|
+
message: "CMMD did not return a complete renewed Forge session.",
|
|
67912
|
+
status: 500
|
|
67913
|
+
});
|
|
67914
|
+
const assertion = yield* validateCloudOwnerCallbackAssertion({
|
|
67915
|
+
serverAuth,
|
|
67916
|
+
assertionToken: credentials.forgeOwnerAssertion,
|
|
67917
|
+
authenticatedUserId: userId
|
|
67918
|
+
});
|
|
67919
|
+
if (!assertion) return yield* die$1(/* @__PURE__ */ new Error("Expected a renewed Forge Cloud owner assertion."));
|
|
67920
|
+
yield* persistCloudOwnerArtifacts({
|
|
67921
|
+
assertion,
|
|
67922
|
+
accessToken: patToStore,
|
|
67923
|
+
identity: cmmdIdentity,
|
|
67924
|
+
jwtToken: credentials.jwtToStore,
|
|
67925
|
+
refreshToken: credentials.refreshToken,
|
|
67926
|
+
sessionGrant,
|
|
67927
|
+
sessionGrantExpiresAt: void 0
|
|
67928
|
+
});
|
|
67929
|
+
return yield* jsonUnsafe({ renewed: true }, { status: 200 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
|
|
67930
|
+
}).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
67600
67931
|
const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", gen(function* () {
|
|
67601
67932
|
const request = yield* HttpServerRequest;
|
|
67602
67933
|
const serverAuth = yield* ServerAuth;
|
|
@@ -67619,14 +67950,24 @@ const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", gen(function* ()
|
|
|
67619
67950
|
sameSite: "lax",
|
|
67620
67951
|
secure
|
|
67621
67952
|
}));
|
|
67622
|
-
if (cloudShell)
|
|
67623
|
-
|
|
67624
|
-
|
|
67625
|
-
|
|
67626
|
-
|
|
67627
|
-
|
|
67628
|
-
|
|
67629
|
-
|
|
67953
|
+
if (cloudShell) {
|
|
67954
|
+
response = yield* response.pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
|
|
67955
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
67956
|
+
httpOnly: true,
|
|
67957
|
+
maxAge: 0,
|
|
67958
|
+
path: "/",
|
|
67959
|
+
sameSite: "lax",
|
|
67960
|
+
secure: true
|
|
67961
|
+
}));
|
|
67962
|
+
response = yield* response.pipe(setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, "", {
|
|
67963
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
67964
|
+
httpOnly: true,
|
|
67965
|
+
maxAge: 0,
|
|
67966
|
+
path: "/api/auth",
|
|
67967
|
+
sameSite: "lax",
|
|
67968
|
+
secure: true
|
|
67969
|
+
}));
|
|
67970
|
+
}
|
|
67630
67971
|
response = yield* pipeExpireLegacySessionCookie(sessions.cookieName, succeed(response), { secure });
|
|
67631
67972
|
if (sessions.cookieName !== "forge_session") response = yield* response.pipe(setCookie(BASE_SESSION_COOKIE_NAME, "", {
|
|
67632
67973
|
expires: /* @__PURE__ */ new Date(0),
|
|
@@ -67649,7 +67990,8 @@ const CommandBootstrapInput = Struct({
|
|
|
67649
67990
|
refreshToken: optionalKey(String$1),
|
|
67650
67991
|
userId: optionalKey(Number$1),
|
|
67651
67992
|
displayName: optionalKey(String$1),
|
|
67652
|
-
user: optionalKey(Unknown)
|
|
67993
|
+
user: optionalKey(Unknown),
|
|
67994
|
+
clientDeviceType: optionalKey(Literals(["desktop", "mobile"]))
|
|
67653
67995
|
});
|
|
67654
67996
|
const CommandOAuthDeviceInput = Struct({
|
|
67655
67997
|
client_id: String$1,
|
|
@@ -68115,6 +68457,11 @@ function commandOAuthProxyStatus(path, status, body) {
|
|
|
68115
68457
|
}
|
|
68116
68458
|
const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", gen(function* () {
|
|
68117
68459
|
const request = yield* HttpServerRequest;
|
|
68460
|
+
const limited = rateLimitOrRespond({
|
|
68461
|
+
routeClass: "auth-bootstrap",
|
|
68462
|
+
clientKey: clientKeyFromRequest(request)
|
|
68463
|
+
});
|
|
68464
|
+
if (limited) return limited;
|
|
68118
68465
|
const serverAuth = yield* ServerAuth;
|
|
68119
68466
|
const sessions = yield* SessionCredentialService;
|
|
68120
68467
|
const payload = yield* schemaBodyJson(AuthBootstrapInput).pipe(mapError((cause) => new AuthError({
|
|
@@ -68132,6 +68479,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", gen(functio
|
|
|
68132
68479
|
}).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68133
68480
|
const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", gen(function* () {
|
|
68134
68481
|
const request = yield* HttpServerRequest;
|
|
68482
|
+
const limited = rateLimitOrRespond({
|
|
68483
|
+
routeClass: "auth-bootstrap",
|
|
68484
|
+
clientKey: clientKeyFromRequest(request)
|
|
68485
|
+
});
|
|
68486
|
+
if (limited) return limited;
|
|
68135
68487
|
const serverAuth = yield* ServerAuth;
|
|
68136
68488
|
const sessions = yield* SessionCredentialService;
|
|
68137
68489
|
if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
|
|
@@ -68180,6 +68532,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
|
|
|
68180
68532
|
}).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68181
68533
|
const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", gen(function* () {
|
|
68182
68534
|
const request = yield* HttpServerRequest;
|
|
68535
|
+
const limited = rateLimitOrRespond({
|
|
68536
|
+
routeClass: "auth-bootstrap",
|
|
68537
|
+
clientKey: clientKeyFromRequest(request)
|
|
68538
|
+
});
|
|
68539
|
+
if (limited) return limited;
|
|
68183
68540
|
const serverAuth = yield* ServerAuth;
|
|
68184
68541
|
const sessions = yield* SessionCredentialService;
|
|
68185
68542
|
if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
|
|
@@ -68197,13 +68554,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68197
68554
|
method: "bearer-session-token",
|
|
68198
68555
|
role: "owner",
|
|
68199
68556
|
subject: sessionSubject,
|
|
68200
|
-
client: {
|
|
68201
|
-
|
|
68202
|
-
|
|
68203
|
-
|
|
68204
|
-
}),
|
|
68205
|
-
deviceType: "desktop"
|
|
68206
|
-
}
|
|
68557
|
+
client: resolveCommandBootstrapClient({
|
|
68558
|
+
request,
|
|
68559
|
+
clientDeviceType: payload.clientDeviceType
|
|
68560
|
+
})
|
|
68207
68561
|
}).pipe(mapError((cause) => new AuthError({
|
|
68208
68562
|
message: "Failed to issue authenticated bearer session.",
|
|
68209
68563
|
cause
|
|
@@ -68213,7 +68567,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68213
68567
|
payload,
|
|
68214
68568
|
cmmdIdentity: validated.cmmdIdentity
|
|
68215
68569
|
});
|
|
68216
|
-
if (!payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject(
|
|
68570
|
+
if ((payload.clientDeviceType ?? "desktop") === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
|
|
68571
|
+
subject: sessionSubject,
|
|
68572
|
+
clientUserAgent: request.headers["user-agent"]
|
|
68573
|
+
}).pipe(mapError((cause) => new AuthError({
|
|
68217
68574
|
message: "Failed to retire legacy desktop refresh credentials.",
|
|
68218
68575
|
status: 500,
|
|
68219
68576
|
cause
|
|
@@ -68228,6 +68585,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68228
68585
|
}, { status: 200 });
|
|
68229
68586
|
}).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68230
68587
|
const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device", gen(function* () {
|
|
68588
|
+
const limited = rateLimitOrRespond({
|
|
68589
|
+
routeClass: "auth-device-poll",
|
|
68590
|
+
clientKey: clientKeyFromRequest(yield* HttpServerRequest)
|
|
68591
|
+
});
|
|
68592
|
+
if (limited) return limited;
|
|
68231
68593
|
const payload = yield* schemaBodyJson(CommandOAuthDeviceInput).pipe(mapError((cause) => new AuthError({
|
|
68232
68594
|
message: "Invalid CMMD OAuth device payload.",
|
|
68233
68595
|
status: 400,
|
|
@@ -68239,7 +68601,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
|
|
|
68239
68601
|
...payload.scope ? { scope: payload.scope } : {}
|
|
68240
68602
|
});
|
|
68241
68603
|
}).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68604
|
+
/**
|
|
68605
|
+
* The device-code poll. Called every `interval` seconds by design (RFC
|
|
68606
|
+
* 8628), which is exactly why FOR-36706 puts a limiter on it: a client that
|
|
68607
|
+
* ignores `slow_down` — a bug, not a person — has no other backstop here.
|
|
68608
|
+
*/
|
|
68242
68609
|
const authCmmdOAuthTokenRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/token", gen(function* () {
|
|
68610
|
+
const limited = rateLimitOrRespond({
|
|
68611
|
+
routeClass: "auth-device-poll",
|
|
68612
|
+
clientKey: clientKeyFromRequest(yield* HttpServerRequest)
|
|
68613
|
+
});
|
|
68614
|
+
if (limited) return limited;
|
|
68243
68615
|
const payload = yield* schemaBodyJson(CommandOAuthTokenInput).pipe(mapError((cause) => new AuthError({
|
|
68244
68616
|
message: "Invalid CMMD OAuth token payload.",
|
|
68245
68617
|
status: 400,
|
|
@@ -68281,6 +68653,14 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
68281
68653
|
authenticatedUserId: userId
|
|
68282
68654
|
});
|
|
68283
68655
|
if (!assertion) return yield* die$1(/* @__PURE__ */ new Error("Expected a Forge Cloud owner assertion in cloud shell mode."));
|
|
68656
|
+
const sessionGrant = resolveCloudOwnerSessionGrantCookie({
|
|
68657
|
+
token: credentials.sessionGrant,
|
|
68658
|
+
expiresAt: credentials.sessionGrantExpiresAt
|
|
68659
|
+
});
|
|
68660
|
+
if (!sessionGrant) return yield* new AuthError({
|
|
68661
|
+
message: "CMMD did not return a durable Forge Cloud session grant.",
|
|
68662
|
+
status: 500
|
|
68663
|
+
});
|
|
68284
68664
|
yield* persistCloudOwnerArtifacts({
|
|
68285
68665
|
assertion,
|
|
68286
68666
|
accessToken: patToStore,
|
|
@@ -68290,10 +68670,12 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
68290
68670
|
sessionGrant: credentials.sessionGrant,
|
|
68291
68671
|
sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
|
|
68292
68672
|
});
|
|
68293
|
-
|
|
68673
|
+
let response = jsonUnsafe({
|
|
68294
68674
|
authenticated: true,
|
|
68295
68675
|
cmmdIdentity
|
|
68296
68676
|
}, { status: 200 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
|
|
68677
|
+
if (sessionGrant) response = response.pipe(flatMap(setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
|
|
68678
|
+
return yield* response;
|
|
68297
68679
|
});
|
|
68298
68680
|
}
|
|
68299
68681
|
const authCmmdPasswordTokenRouteLayer = add$1("POST", "/api/auth/cmmd-password/token", gen(function* () {
|
|
@@ -68467,6 +68849,14 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
68467
68849
|
assertionToken: forgeOwnerAssertion,
|
|
68468
68850
|
authenticatedUserId: userId
|
|
68469
68851
|
});
|
|
68852
|
+
const cloudSessionGrant = cloudOwnerAssertion ? resolveCloudOwnerSessionGrantCookie({
|
|
68853
|
+
token: sessionGrant,
|
|
68854
|
+
expiresAt: sessionGrantExpiresAt
|
|
68855
|
+
}) : null;
|
|
68856
|
+
if (cloudOwnerAssertion && !cloudSessionGrant) return yield* new AuthError({
|
|
68857
|
+
message: "CMMD did not return a durable Forge Cloud session grant.",
|
|
68858
|
+
status: 500
|
|
68859
|
+
});
|
|
68470
68860
|
const sessionSubject = resolveCommandOAuthSubject(userId);
|
|
68471
68861
|
const issued = cloudOwnerAssertion ? null : yield* sessions.issue({
|
|
68472
68862
|
method: "browser-session-cookie",
|
|
@@ -68498,7 +68888,7 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
68498
68888
|
else yield* persistLocalOAuthArtifacts(persistenceInput);
|
|
68499
68889
|
const homeUrl = `${publicUrl}/`;
|
|
68500
68890
|
const secureCookie = shouldUseSecureCookie(publicUrl);
|
|
68501
|
-
if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion);
|
|
68891
|
+
if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion, cloudSessionGrant);
|
|
68502
68892
|
if (!issued) return yield* die$1(/* @__PURE__ */ new Error("Expected a local session outside Forge Cloud."));
|
|
68503
68893
|
return yield* pipeExpireLegacySessionCookie(sessions.cookieName, redirect(homeUrl, { status: 302 }).pipe(setCookie(sessions.cookieName, issued.token, {
|
|
68504
68894
|
expires: toDate(issued.expiresAt),
|
|
@@ -68516,6 +68906,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
68516
68906
|
}).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68517
68907
|
const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", gen(function* () {
|
|
68518
68908
|
const request = yield* HttpServerRequest;
|
|
68909
|
+
const limited = rateLimitOrRespond({
|
|
68910
|
+
routeClass: "auth-bootstrap",
|
|
68911
|
+
clientKey: clientKeyFromRequest(request)
|
|
68912
|
+
});
|
|
68913
|
+
if (limited) return limited;
|
|
68519
68914
|
const serverAuth = yield* ServerAuth;
|
|
68520
68915
|
const payload = yield* schemaBodyJson(AuthBootstrapInput).pipe(mapError((cause) => new AuthError({
|
|
68521
68916
|
message: "Invalid bootstrap payload.",
|
|
@@ -68547,6 +68942,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
|
|
|
68547
68942
|
*/
|
|
68548
68943
|
const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", gen(function* () {
|
|
68549
68944
|
const request = yield* HttpServerRequest;
|
|
68945
|
+
const limited = rateLimitOrRespond({
|
|
68946
|
+
routeClass: "auth-token-exchange",
|
|
68947
|
+
clientKey: clientKeyFromRequest(request)
|
|
68948
|
+
});
|
|
68949
|
+
if (limited) return limited;
|
|
68550
68950
|
const serverAuth = yield* ServerAuth;
|
|
68551
68951
|
const payload = yield* schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(mapError((cause) => new AuthError({
|
|
68552
68952
|
message: "Invalid token exchange payload.",
|
|
@@ -70518,6 +70918,11 @@ function redeemCmmdSessionGrant(grantToken) {
|
|
|
70518
70918
|
};
|
|
70519
70919
|
}).catch(() => null);
|
|
70520
70920
|
}
|
|
70921
|
+
const REJECTED_OAUTH_ERROR_CODES = new Set([
|
|
70922
|
+
"invalid_grant",
|
|
70923
|
+
"invalid_client",
|
|
70924
|
+
"invalid_request"
|
|
70925
|
+
]);
|
|
70521
70926
|
/**
|
|
70522
70927
|
* Rotate a CMMD refresh token into a fresh access JWT.
|
|
70523
70928
|
*
|
|
@@ -70530,7 +70935,7 @@ function redeemCmmdSessionGrant(grantToken) {
|
|
|
70530
70935
|
* code are what identify the failure, and `invalid_grant` in particular means
|
|
70531
70936
|
* the token was already spent, which is a different fix from a 500.
|
|
70532
70937
|
*/
|
|
70533
|
-
function refreshCmmdJwt(refreshToken) {
|
|
70938
|
+
function refreshCmmdJwt(refreshToken, observer) {
|
|
70534
70939
|
return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
|
|
70535
70940
|
method: "POST",
|
|
70536
70941
|
headers: { "Content-Type": "application/json" },
|
|
@@ -70541,7 +70946,9 @@ function refreshCmmdJwt(refreshToken) {
|
|
|
70541
70946
|
})
|
|
70542
70947
|
}).then(async (response) => {
|
|
70543
70948
|
if (!response.ok) {
|
|
70544
|
-
|
|
70949
|
+
const errorCode = await readOAuthErrorCode(response);
|
|
70950
|
+
if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
|
|
70951
|
+
console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
|
|
70545
70952
|
return null;
|
|
70546
70953
|
}
|
|
70547
70954
|
const parsed = parseCmmdOAuthRefreshResponse(await response.json());
|
|
@@ -70553,20 +70960,73 @@ function refreshCmmdJwt(refreshToken) {
|
|
|
70553
70960
|
});
|
|
70554
70961
|
}
|
|
70555
70962
|
/**
|
|
70556
|
-
* The OAuth `error` code from a refusal,
|
|
70963
|
+
* The OAuth `error` code from a refusal, lowercased, or null when there is none.
|
|
70557
70964
|
*
|
|
70558
70965
|
* Best effort by design: a body that is not JSON, or is already consumed, must
|
|
70559
|
-
* not turn a logged failure into an unlogged one.
|
|
70966
|
+
* not turn a logged failure into an unlogged one. Null also reads correctly as
|
|
70967
|
+
* "no evidence the credential is dead", which is the safe answer for a caller
|
|
70968
|
+
* deciding whether to keep a refresh token.
|
|
70560
70969
|
*/
|
|
70561
|
-
async function
|
|
70970
|
+
async function readOAuthErrorCode(response) {
|
|
70562
70971
|
try {
|
|
70563
70972
|
const code = (await response.json())["error"];
|
|
70564
|
-
return typeof code === "string" && code.trim().length > 0 ?
|
|
70973
|
+
return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
|
|
70565
70974
|
} catch {
|
|
70566
|
-
return
|
|
70975
|
+
return null;
|
|
70567
70976
|
}
|
|
70568
70977
|
}
|
|
70569
70978
|
//#endregion
|
|
70979
|
+
//#region src/command/cmmdRotationFence.ts
|
|
70980
|
+
/**
|
|
70981
|
+
* True when Forge must not rotate this session's CMMD credentials.
|
|
70982
|
+
*
|
|
70983
|
+
* Fails closed: if the session row cannot be read, assume the fence applies.
|
|
70984
|
+
* Spending a token Forge had no right to spend is worse than one extra
|
|
70985
|
+
* reconnect.
|
|
70986
|
+
*/
|
|
70987
|
+
function isRotationFencedSession(sessionId) {
|
|
70988
|
+
if (readCommandRefreshTokenForSession(sessionId)) return succeed(false);
|
|
70989
|
+
return isDesktopBearerAuthSession(sessionId).pipe(orElseSucceed(() => true));
|
|
70990
|
+
}
|
|
70991
|
+
//#endregion
|
|
70992
|
+
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
70993
|
+
/**
|
|
70994
|
+
* How long a recorded rejection stays usable.
|
|
70995
|
+
*
|
|
70996
|
+
* Long enough to span the resolver ladder and the response that follows it,
|
|
70997
|
+
* short enough that it cannot outlive the credential it describes.
|
|
70998
|
+
*/
|
|
70999
|
+
const REJECTION_TTL_MS = 6e4;
|
|
71000
|
+
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71001
|
+
/** Record that CMMD refused this session's credential outright. */
|
|
71002
|
+
function noteCmmdCredentialRejected(sessionId) {
|
|
71003
|
+
rejectedAt.set(sessionId, Date.now());
|
|
71004
|
+
}
|
|
71005
|
+
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71006
|
+
function hasCmmdCredentialRejection(sessionId) {
|
|
71007
|
+
const observedAt = rejectedAt.get(sessionId);
|
|
71008
|
+
if (observedAt === void 0) return false;
|
|
71009
|
+
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71010
|
+
rejectedAt.delete(sessionId);
|
|
71011
|
+
return false;
|
|
71012
|
+
}
|
|
71013
|
+
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71014
|
+
function forgetCmmdCredentialRejection(sessionId) {
|
|
71015
|
+
rejectedAt.delete(sessionId);
|
|
71016
|
+
}
|
|
71017
|
+
/**
|
|
71018
|
+
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71019
|
+
*
|
|
71020
|
+
* This is the only door the 401 path may use. A resolver that returned null
|
|
71021
|
+
* because it was not allowed to try is a policy refusal and leaves the stored
|
|
71022
|
+
* credential exactly where it is, so the next request can rotate it.
|
|
71023
|
+
*/
|
|
71024
|
+
const clearCmmdAuthAfterCmmdRejection = (sessionId) => gen(function* () {
|
|
71025
|
+
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71026
|
+
yield* clearUnrecoverableCmmdAuthForSession(sessionId);
|
|
71027
|
+
forgetCmmdCredentialRejection(sessionId);
|
|
71028
|
+
});
|
|
71029
|
+
//#endregion
|
|
70570
71030
|
//#region src/command/cmmdJwtResolver.ts
|
|
70571
71031
|
function usableCmmdUserJwt(token) {
|
|
70572
71032
|
return isCmmdUserAccessJwt(token) ? token : null;
|
|
@@ -70579,7 +71039,7 @@ function refreshOnceForSession(sessionId, refreshToken) {
|
|
|
70579
71039
|
return promise(() => {
|
|
70580
71040
|
const existing = inFlightRefreshes.get(sessionId);
|
|
70581
71041
|
if (existing) return existing;
|
|
70582
|
-
const pending = refreshCmmdJwt(refreshToken).catch(() => null).finally(() => {
|
|
71042
|
+
const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
70583
71043
|
inFlightRefreshes.delete(sessionId);
|
|
70584
71044
|
});
|
|
70585
71045
|
inFlightRefreshes.set(sessionId, pending);
|
|
@@ -70689,13 +71149,16 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
|
|
|
70689
71149
|
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
70690
71150
|
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
70691
71151
|
*/
|
|
70692
|
-
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
|
|
71152
|
+
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
70693
71153
|
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
70694
|
-
if (
|
|
71154
|
+
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
71155
|
+
if (!forceRefresh) return succeed(cached);
|
|
71156
|
+
if (staleAccessToken && cached !== staleAccessToken) return succeed(cached);
|
|
71157
|
+
}
|
|
70695
71158
|
const normalizedSubject = subject?.trim() || null;
|
|
70696
71159
|
if (!normalizedSubject && !forceRefresh) return succeed(null);
|
|
70697
71160
|
return gen(function* () {
|
|
70698
|
-
if (yield*
|
|
71161
|
+
if (yield* isRotationFencedSession(sessionId)) return null;
|
|
70699
71162
|
if (forceRefresh) {
|
|
70700
71163
|
const forced = yield* tryOwnCredentialRecovery(sessionId, logPrefix);
|
|
70701
71164
|
if (forced) return forced;
|
|
@@ -71454,15 +71917,25 @@ function clearCmmdAuthAfterFailure(sessionId) {
|
|
|
71454
71917
|
})));
|
|
71455
71918
|
}
|
|
71456
71919
|
/**
|
|
71457
|
-
* The single "no usable CMMD credential" response:
|
|
71458
|
-
*
|
|
71459
|
-
*
|
|
71460
|
-
*
|
|
71461
|
-
*
|
|
71462
|
-
*
|
|
71920
|
+
* The single "no usable CMMD credential" response: returns the canonical
|
|
71921
|
+
* `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the session has
|
|
71922
|
+
* no way to reach CMMD — a missing PAT, a JWT that fails to resolve, a bespoke
|
|
71923
|
+
* gate elsewhere in http.ts — emits this same response so the Shell only ever
|
|
71924
|
+
* has one `CMMD_AUTH_REQUIRED` body to branch on.
|
|
71925
|
+
*
|
|
71926
|
+
* It used to delete the session's stored credentials on the way out, and that
|
|
71927
|
+
* was the wrong reading of its own argument. Most of the paths that reach here
|
|
71928
|
+
* are refusals, not rejections: the resolver was fenced, the subject was
|
|
71929
|
+
* missing, a gate said no. A phone would arrive holding a refresh token good
|
|
71930
|
+
* for the next three months, be refused by a rule, and have that token deleted
|
|
71931
|
+
* as "unrecoverable". The clear now happens only where CMMD has actually
|
|
71932
|
+
* refused the credential, which `clearCmmdAuthAfterCmmdRejection` checks.
|
|
71463
71933
|
*/
|
|
71464
71934
|
function respondCmmdConnectionRequired(sessionId) {
|
|
71465
|
-
return
|
|
71935
|
+
return clearCmmdAuthAfterCmmdRejection(sessionId).pipe(catch_((error) => logWarning$1("Failed to clear CMMD auth that CMMD itself rejected.", {
|
|
71936
|
+
cause: error,
|
|
71937
|
+
sessionId
|
|
71938
|
+
})), as$3(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
|
|
71466
71939
|
}
|
|
71467
71940
|
async function dispatchToProxy(method, jwt, cmmdPath, rawBody, orgId, searchParams) {
|
|
71468
71941
|
return cmmdGateway.request({
|
|
@@ -71518,6 +71991,11 @@ function buildProxyRouteLayer(route) {
|
|
|
71518
71991
|
const handler = gen(function* () {
|
|
71519
71992
|
const req = yield* HttpServerRequest;
|
|
71520
71993
|
const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
|
|
71994
|
+
const limited = rateLimitOrRespond({
|
|
71995
|
+
routeClass: "cmmd-proxy",
|
|
71996
|
+
clientKey: session.sessionId
|
|
71997
|
+
});
|
|
71998
|
+
if (limited) return limited;
|
|
71521
71999
|
const pathParams = yield* params;
|
|
71522
72000
|
const cmmdPath = substitutePath(route.cmmdPath, pathParams);
|
|
71523
72001
|
if (!cmmdPath) return jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
|
|
@@ -71577,6 +72055,11 @@ function buildProxyHandlerEffect(route) {
|
|
|
71577
72055
|
return gen(function* () {
|
|
71578
72056
|
const req = yield* HttpServerRequest;
|
|
71579
72057
|
const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
|
|
72058
|
+
const limited = rateLimitOrRespond({
|
|
72059
|
+
routeClass: "cmmd-proxy",
|
|
72060
|
+
clientKey: session.sessionId
|
|
72061
|
+
});
|
|
72062
|
+
if (limited) return limited;
|
|
71580
72063
|
if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
|
|
71581
72064
|
const parsed = yield* route.parse(req);
|
|
71582
72065
|
if (!parsed.ok) return jsonUnsafe(parsed.body, { status: parsed.status });
|
|
@@ -75215,7 +75698,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
75215
75698
|
var fs$6 = __require("fs");
|
|
75216
75699
|
var path$5 = __require("path");
|
|
75217
75700
|
var url$1 = __require("url");
|
|
75218
|
-
var os$
|
|
75701
|
+
var os$3 = __require("os");
|
|
75219
75702
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
75220
75703
|
var vars = process.config && process.config.variables || {};
|
|
75221
75704
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
@@ -75223,8 +75706,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
75223
75706
|
var abi = versions.modules;
|
|
75224
75707
|
if (versions.deno || process.isBun) abi = "unsupported";
|
|
75225
75708
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
75226
|
-
var arch = process.env.npm_config_arch || os$
|
|
75227
|
-
var platform = process.env.npm_config_platform || os$
|
|
75709
|
+
var arch = process.env.npm_config_arch || os$3.arch();
|
|
75710
|
+
var platform = process.env.npm_config_platform || os$3.platform();
|
|
75228
75711
|
var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
|
|
75229
75712
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
75230
75713
|
var uv = (versions.uv || "").split(".")[0];
|
|
@@ -83346,12 +83829,12 @@ const makePullRequestService = gen(function* () {
|
|
|
83346
83829
|
}));
|
|
83347
83830
|
const submitReview = (input) => requireProject(input).pipe(flatMap((project) => {
|
|
83348
83831
|
const review = project.api.capabilities.review;
|
|
83349
|
-
if (!review.verdicts.includes(input.verdict)) return refuse(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
83350
|
-
if (input.comments.length > 0 && !review.inlineComment) return refuse("This host cannot comment on a line of a change request.");
|
|
83351
|
-
if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse("A review needs a summary or at least one comment.");
|
|
83832
|
+
if (!review.verdicts.includes(input.verdict)) return refuse$1(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
83833
|
+
if (input.comments.length > 0 && !review.inlineComment) return refuse$1("This host cannot comment on a line of a change request.");
|
|
83834
|
+
if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse$1("A review needs a summary or at least one comment.");
|
|
83352
83835
|
return viewerPermissionsOf(project, input, "submitReview").pipe(flatMap((viewer) => {
|
|
83353
|
-
if (!viewer.verdicts.includes(input.verdict)) return refuse(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
83354
|
-
if (input.comments.length > 0 && !viewer.comment) return refuse("You need write access on this repository to comment on a line of a change request.");
|
|
83836
|
+
if (!viewer.verdicts.includes(input.verdict)) return refuse$1(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
83837
|
+
if (input.comments.length > 0 && !viewer.comment) return refuse$1("You need write access on this repository to comment on a line of a change request.");
|
|
83355
83838
|
return project.api.submitReview({
|
|
83356
83839
|
projectId: project.project.id,
|
|
83357
83840
|
cwd: project.project.workspaceRoot,
|
|
@@ -83748,7 +84231,7 @@ const toEntry = (input) => {
|
|
|
83748
84231
|
...input.item.reviewDecision === void 0 || input.item.reviewDecision === null ? {} : { reviewDecision: input.item.reviewDecision }
|
|
83749
84232
|
};
|
|
83750
84233
|
};
|
|
83751
|
-
const refuse = (detail) => fail(new PullRequestOperationError({
|
|
84234
|
+
const refuse$1 = (detail) => fail(new PullRequestOperationError({
|
|
83752
84235
|
operation: "submitReview",
|
|
83753
84236
|
detail
|
|
83754
84237
|
}));
|
|
@@ -93406,7 +93889,7 @@ const launchDetached = (launch) => gen(function* () {
|
|
|
93406
93889
|
});
|
|
93407
93890
|
const OpenLive = effect(Open, gen(function* () {
|
|
93408
93891
|
const open = yield* tryPromise({
|
|
93409
|
-
try: () => import("./open-
|
|
93892
|
+
try: () => import("./open-FIgTDosf.mjs"),
|
|
93410
93893
|
catch: (cause) => new OpenError$1({
|
|
93411
93894
|
message: "failed to load browser opener",
|
|
93412
93895
|
cause
|
|
@@ -93590,6 +94073,166 @@ const admitNormalizedDispatchCommand = (command, admit) => uninterruptibleMask$1
|
|
|
93590
94073
|
};
|
|
93591
94074
|
}));
|
|
93592
94075
|
//#endregion
|
|
94076
|
+
//#region src/orchestration/threadDetailWindow.ts
|
|
94077
|
+
function earliest(current, candidate) {
|
|
94078
|
+
return current === void 0 || candidate < current ? candidate : current;
|
|
94079
|
+
}
|
|
94080
|
+
/**
|
|
94081
|
+
* Every turn in the thread, oldest first.
|
|
94082
|
+
*
|
|
94083
|
+
* Ordering is by the earliest timestamp across all four windowed collections
|
|
94084
|
+
* rather than by message order alone: a turn can open with an activity (a tool
|
|
94085
|
+
* call before any text) and a checkpoint-only turn has no message at all.
|
|
94086
|
+
* Ties break on turn id so the order is total and a cursor means the same thing
|
|
94087
|
+
* on every read.
|
|
94088
|
+
*/
|
|
94089
|
+
function orderThreadTurns(thread) {
|
|
94090
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
94091
|
+
const touch = (turnId, at, isUserMessage) => {
|
|
94092
|
+
if (turnId === null) return;
|
|
94093
|
+
const existing = anchors.get(turnId);
|
|
94094
|
+
anchors.set(turnId, {
|
|
94095
|
+
at: earliest(existing?.at, at),
|
|
94096
|
+
hasUserMessage: (existing?.hasUserMessage ?? false) || isUserMessage
|
|
94097
|
+
});
|
|
94098
|
+
};
|
|
94099
|
+
for (const message of thread.messages) touch(message.turnId, message.createdAt, message.role === "user");
|
|
94100
|
+
for (const activity of thread.activities) touch(activity.turnId, activity.createdAt, false);
|
|
94101
|
+
for (const plan of thread.proposedPlans) touch(plan.turnId, plan.createdAt, false);
|
|
94102
|
+
for (const checkpoint of thread.checkpoints) touch(checkpoint.turnId, checkpoint.completedAt, false);
|
|
94103
|
+
return Array.from(anchors, ([turnId, anchor]) => ({
|
|
94104
|
+
turnId,
|
|
94105
|
+
...anchor
|
|
94106
|
+
})).toSorted((left, right) => left.at === right.at ? left.turnId.localeCompare(right.turnId) : left.at < right.at ? -1 : 1);
|
|
94107
|
+
}
|
|
94108
|
+
/**
|
|
94109
|
+
* Index of the first turn to include, counting back `turnLimit` user-anchored
|
|
94110
|
+
* turns from the end of `ordered`. Returns 0 when the thread holds fewer user
|
|
94111
|
+
* turns than the limit, which is how a short thread comes back whole.
|
|
94112
|
+
*/
|
|
94113
|
+
function windowStartIndex(ordered, turnLimit) {
|
|
94114
|
+
let userTurns = 0;
|
|
94115
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
94116
|
+
const anchor = ordered[index];
|
|
94117
|
+
if (anchor !== void 0 && anchor.hasUserMessage) {
|
|
94118
|
+
userTurns += 1;
|
|
94119
|
+
if (userTurns === turnLimit) return index;
|
|
94120
|
+
}
|
|
94121
|
+
}
|
|
94122
|
+
return 0;
|
|
94123
|
+
}
|
|
94124
|
+
/**
|
|
94125
|
+
* The turns a windowed read returns.
|
|
94126
|
+
*
|
|
94127
|
+
* `beforeCursor` bounds the read to turns strictly older than a cursor an
|
|
94128
|
+
* earlier read handed out, which is what "load earlier" sends back. A cursor
|
|
94129
|
+
* naming a turn this thread no longer holds, after a revert, say, yields an
|
|
94130
|
+
* empty page rather than the whole thread: returning everything would be a
|
|
94131
|
+
* silent full download at the exact moment the client asked for less.
|
|
94132
|
+
*/
|
|
94133
|
+
function selectWindowTurnIds(ordered, window) {
|
|
94134
|
+
const cursorIndex = window.beforeCursor === void 0 ? ordered.length : ordered.findIndex((anchor) => anchor.turnId === window.beforeCursor);
|
|
94135
|
+
if (cursorIndex < 0) return {
|
|
94136
|
+
turnIds: /* @__PURE__ */ new Set(),
|
|
94137
|
+
beforeCursor: null,
|
|
94138
|
+
hasMore: false,
|
|
94139
|
+
includeUnanchored: false
|
|
94140
|
+
};
|
|
94141
|
+
const candidates = ordered.slice(0, cursorIndex);
|
|
94142
|
+
const startIndex = window.turnLimit === void 0 ? 0 : windowStartIndex(candidates, window.turnLimit);
|
|
94143
|
+
const included = candidates.slice(startIndex);
|
|
94144
|
+
const oldest = included[0];
|
|
94145
|
+
return {
|
|
94146
|
+
turnIds: new Set(included.map((anchor) => anchor.turnId)),
|
|
94147
|
+
beforeCursor: oldest?.turnId ?? null,
|
|
94148
|
+
hasMore: startIndex > 0,
|
|
94149
|
+
includeUnanchored: startIndex === 0
|
|
94150
|
+
};
|
|
94151
|
+
}
|
|
94152
|
+
function keepsTurn(selection, turnId) {
|
|
94153
|
+
return turnId === null ? selection.includeUnanchored : selection.turnIds.has(turnId);
|
|
94154
|
+
}
|
|
94155
|
+
/**
|
|
94156
|
+
* Applies a turn window to a full thread detail read.
|
|
94157
|
+
*
|
|
94158
|
+
* A request with neither field is a full read and comes back untouched with no
|
|
94159
|
+
* page, which is exactly what a pre-pagination client already receives. Only
|
|
94160
|
+
* the four turn-scoped collections are filtered; thread metadata, the session,
|
|
94161
|
+
* and `latestTurn` describe the thread as a whole and stay as they are, so a
|
|
94162
|
+
* windowed snapshot still renders a correct header and a correct running state.
|
|
94163
|
+
*/
|
|
94164
|
+
function windowThreadDetail(input) {
|
|
94165
|
+
const { thread, window, snapshotSequence } = input;
|
|
94166
|
+
if (window.turnLimit === void 0 && window.beforeCursor === void 0) return {
|
|
94167
|
+
thread,
|
|
94168
|
+
page: void 0
|
|
94169
|
+
};
|
|
94170
|
+
const selection = selectWindowTurnIds(orderThreadTurns(thread), window);
|
|
94171
|
+
return {
|
|
94172
|
+
thread: {
|
|
94173
|
+
...thread,
|
|
94174
|
+
messages: thread.messages.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94175
|
+
activities: thread.activities.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94176
|
+
proposedPlans: thread.proposedPlans.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94177
|
+
checkpoints: thread.checkpoints.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94178
|
+
...thread.turnModelSelections === void 0 ? {} : { turnModelSelections: thread.turnModelSelections.filter((row) => keepsTurn(selection, row.turnId)) }
|
|
94179
|
+
},
|
|
94180
|
+
page: {
|
|
94181
|
+
beforeCursor: selection.hasMore ? selection.beforeCursor : null,
|
|
94182
|
+
hasMore: selection.hasMore,
|
|
94183
|
+
snapshotSequence
|
|
94184
|
+
}
|
|
94185
|
+
};
|
|
94186
|
+
}
|
|
94187
|
+
/** Whether a subscriber's cursor can be served by replay rather than a snapshot. */
|
|
94188
|
+
function canResumeFromSequence(input) {
|
|
94189
|
+
if (input.afterSequence > input.currentSequence) return false;
|
|
94190
|
+
return input.currentSequence - input.afterSequence <= (input.maxGap ?? 2e3);
|
|
94191
|
+
}
|
|
94192
|
+
function snapshotCatchUp(snapshot) {
|
|
94193
|
+
return {
|
|
94194
|
+
items: [{
|
|
94195
|
+
kind: "snapshot",
|
|
94196
|
+
snapshot
|
|
94197
|
+
}],
|
|
94198
|
+
watermark: snapshot.snapshotSequence
|
|
94199
|
+
};
|
|
94200
|
+
}
|
|
94201
|
+
function replayCatchUp(events, afterSequence) {
|
|
94202
|
+
return {
|
|
94203
|
+
items: events.map((event) => ({
|
|
94204
|
+
kind: "event",
|
|
94205
|
+
event
|
|
94206
|
+
})),
|
|
94207
|
+
watermark: events.at(-1)?.sequence ?? afterSequence
|
|
94208
|
+
};
|
|
94209
|
+
}
|
|
94210
|
+
/**
|
|
94211
|
+
* Builds the frames that precede live events: either the replayed catch-up or
|
|
94212
|
+
* the snapshot, plus the completion marker when one was asked for.
|
|
94213
|
+
*/
|
|
94214
|
+
const buildCatchUp = fn$1("threadSubscription.buildCatchUp")(function* (input, sources) {
|
|
94215
|
+
const afterSequence = input.afterSequence;
|
|
94216
|
+
if (afterSequence === void 0) return snapshotCatchUp(yield* sources.loadSnapshot);
|
|
94217
|
+
if (!canResumeFromSequence({
|
|
94218
|
+
afterSequence,
|
|
94219
|
+
currentSequence: yield* sources.currentSequence
|
|
94220
|
+
})) return snapshotCatchUp(yield* sources.loadSnapshot);
|
|
94221
|
+
const replayed = yield* runCollect(sources.replayEvents(afterSequence));
|
|
94222
|
+
return replayCatchUp(Array.from(replayed), afterSequence);
|
|
94223
|
+
});
|
|
94224
|
+
function makeThreadSubscriptionStream(input, sources) {
|
|
94225
|
+
return unwrap$1(gen(function* () {
|
|
94226
|
+
const liveEvents = yield* sources.subscribeLiveEvents;
|
|
94227
|
+
const catchUp = yield* buildCatchUp(input, sources);
|
|
94228
|
+
const marker = input.requestCompletionMarker === true ? [{ kind: "synchronized" }] : [];
|
|
94229
|
+
return concat$2(fromArray([...catchUp.items, ...marker]), liveEvents.pipe(filter$2((event) => event.sequence > catchUp.watermark), map$7((event) => ({
|
|
94230
|
+
kind: "event",
|
|
94231
|
+
event
|
|
94232
|
+
}))));
|
|
94233
|
+
}));
|
|
94234
|
+
}
|
|
94235
|
+
//#endregion
|
|
93593
94236
|
//#region src/orchestration/bootstrapCommandCoordinator.ts
|
|
93594
94237
|
const MAX_COMPLETED_COMMANDS_PER_ENGINE = 256;
|
|
93595
94238
|
const commandsByEngine = /* @__PURE__ */ new WeakMap();
|
|
@@ -97243,6 +97886,26 @@ function serializeNativeToolActivity(activity) {
|
|
|
97243
97886
|
}
|
|
97244
97887
|
};
|
|
97245
97888
|
}
|
|
97889
|
+
function serializeWorktreeLaunchActivity(activity) {
|
|
97890
|
+
if (decodeWorktreeLaunchActivityKind(activity.kind) === null) return null;
|
|
97891
|
+
const summary = text(activity.summary, 4e3);
|
|
97892
|
+
if (summary === null) return null;
|
|
97893
|
+
return {
|
|
97894
|
+
sourceSequence: null,
|
|
97895
|
+
event: {
|
|
97896
|
+
eventId: `activity:${activity.id}`,
|
|
97897
|
+
occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
|
|
97898
|
+
...eventTurn(activity.turnId),
|
|
97899
|
+
kind: "tool_call",
|
|
97900
|
+
summary,
|
|
97901
|
+
status: "completed",
|
|
97902
|
+
payload: encodeCmmdRuntimeWorktreeLaunchActivityPayload(activity.kind, activity.payload)
|
|
97903
|
+
}
|
|
97904
|
+
};
|
|
97905
|
+
}
|
|
97906
|
+
function decodeWorktreeLaunchActivityKind(kind) {
|
|
97907
|
+
return isCmmdRuntimeWorktreeLaunchActivityKind(kind) ? kind : null;
|
|
97908
|
+
}
|
|
97246
97909
|
function isReplicatedFailureActivity(activity) {
|
|
97247
97910
|
return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
|
|
97248
97911
|
}
|
|
@@ -97623,12 +98286,17 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
97623
98286
|
const event = serializeNativeToolActivity(activity);
|
|
97624
98287
|
return event === null ? [] : [event];
|
|
97625
98288
|
});
|
|
98289
|
+
const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
|
|
98290
|
+
const event = serializeWorktreeLaunchActivity(activity);
|
|
98291
|
+
return event === null ? [] : [event];
|
|
98292
|
+
});
|
|
97626
98293
|
const recordedTurns = serializeRecordedTurns(thread);
|
|
97627
98294
|
const textGenerations = serializeTextGenerations(thread);
|
|
97628
98295
|
return [
|
|
97629
98296
|
...messages,
|
|
97630
98297
|
...replicaActivities,
|
|
97631
98298
|
...toolActivities,
|
|
98299
|
+
...worktreeLaunchActivities,
|
|
97632
98300
|
...failureActivities,
|
|
97633
98301
|
...recordedTurns,
|
|
97634
98302
|
...textGenerations
|
|
@@ -101904,7 +102572,7 @@ function claudeReadinessReasonCode(reason) {
|
|
|
101904
102572
|
//#endregion
|
|
101905
102573
|
//#region src/egress/providerDeliveryReceipt.ts
|
|
101906
102574
|
const PROVIDER_ACCOUNT_IDENTITY$2 = /^pai_v1_[A-Za-z0-9_-]{43}$/;
|
|
101907
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
102575
|
+
const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
101908
102576
|
const RFC3339$1 = /^(\d{4})-(\d{2})-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
101909
102577
|
const RECEIPT_KEYS = new Set([
|
|
101910
102578
|
"receiptId",
|
|
@@ -101987,7 +102655,7 @@ function claudeAccountFields(value) {
|
|
|
101987
102655
|
}
|
|
101988
102656
|
function parseClaudeDeliveryIdentity(value) {
|
|
101989
102657
|
const account = claudeAccountFields(value.providerAccountIdentity);
|
|
101990
|
-
if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
|
|
102658
|
+
if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID$1.test(value.credentialGenerationId)) return null;
|
|
101991
102659
|
return {
|
|
101992
102660
|
kind: "credential_generation",
|
|
101993
102661
|
provider: "claude",
|
|
@@ -102754,7 +103422,10 @@ function extractClaudeProviderAccountId(response) {
|
|
|
102754
103422
|
if (typeof response !== "object" || response === null || !("data" in response)) return null;
|
|
102755
103423
|
const data = response.data;
|
|
102756
103424
|
if (typeof data !== "object" || data === null) return null;
|
|
102757
|
-
if ("connected" in data && data.connected === true)
|
|
103425
|
+
if ("connected" in data && data.connected === true) {
|
|
103426
|
+
if ("providerAccountId" in data) return asNonEmptyString$1(data.providerAccountId) ?? null;
|
|
103427
|
+
return extractClaudeCredentialGenerationId(response);
|
|
103428
|
+
}
|
|
102758
103429
|
if (!("account" in data)) return null;
|
|
102759
103430
|
const account = data.account;
|
|
102760
103431
|
if (typeof account !== "object" || account === null || !("providerAccountId" in account) || typeof account.providerAccountId !== "string") return null;
|
|
@@ -105726,6 +106397,14 @@ function makeRelayCommandInboxRepository(sql) {
|
|
|
105726
106397
|
function isThreadDetailEvent(event) {
|
|
105727
106398
|
return event.type === "thread.message-sent" || event.type === "thread.proposed-plan-upserted" || event.type === "thread.activity-appended" || event.type === "thread.turn-diff-completed" || event.type === "thread.reverted" || event.type === "thread.session-set" || event.type === "thread.rate-limit-set";
|
|
105728
106399
|
}
|
|
106400
|
+
/**
|
|
106401
|
+
* Narrows the domain-event feed to one thread's stream-visible events. The same
|
|
106402
|
+
* predicate guards the live feed and the resume replay, so a subscriber cannot
|
|
106403
|
+
* be handed a different set of events depending on which path served it.
|
|
106404
|
+
*/
|
|
106405
|
+
function isThreadDetailEventFor(threadId) {
|
|
106406
|
+
return (event) => event.aggregateKind === "thread" && event.aggregateId === threadId && isThreadDetailEvent(event);
|
|
106407
|
+
}
|
|
105729
106408
|
const PROVIDER_STATUS_DEBOUNCE_MS = 200;
|
|
105730
106409
|
function streamBoundedText(contents) {
|
|
105731
106410
|
if (Buffer.byteLength(contents, "utf8") > 16777216) return fail(new BulkTransferError({
|
|
@@ -105771,6 +106450,39 @@ function loadInitialShellSnapshot(input) {
|
|
|
105771
106450
|
return yield* input.projectionSnapshotQuery.getShellSnapshot();
|
|
105772
106451
|
});
|
|
105773
106452
|
}
|
|
106453
|
+
/**
|
|
106454
|
+
* The thread snapshot a subscription falls back to, windowed when the
|
|
106455
|
+
* subscriber asked for a window. A missing thread is a failure rather than an
|
|
106456
|
+
* empty snapshot: the subscriber asked about a thread this server does not
|
|
106457
|
+
* have, and saying so is the only honest answer.
|
|
106458
|
+
*
|
|
106459
|
+
* Also the read behind `getThreadDetailPage`, the socket RPC a relay-only
|
|
106460
|
+
* connection uses for "load earlier turns" in place of the HTTP snapshot
|
|
106461
|
+
* route: same window, same pure `windowThreadDetail`, so the two reads cannot
|
|
106462
|
+
* disagree about what a page is.
|
|
106463
|
+
*/
|
|
106464
|
+
function loadThreadDetailSnapshot(input) {
|
|
106465
|
+
return gen(function* () {
|
|
106466
|
+
const [threadDetail, snapshotSequence] = yield* all([input.projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
|
|
106467
|
+
message: `Failed to load thread ${input.threadId}`,
|
|
106468
|
+
cause
|
|
106469
|
+
}))), input.orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
|
|
106470
|
+
if (isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
|
|
106471
|
+
message: `Thread ${input.threadId} was not found`,
|
|
106472
|
+
cause: input.threadId
|
|
106473
|
+
});
|
|
106474
|
+
const windowed = windowThreadDetail({
|
|
106475
|
+
thread: threadDetail.value,
|
|
106476
|
+
window: input.window,
|
|
106477
|
+
snapshotSequence
|
|
106478
|
+
});
|
|
106479
|
+
return {
|
|
106480
|
+
snapshotSequence,
|
|
106481
|
+
thread: windowed.thread,
|
|
106482
|
+
...windowed.page === void 0 ? {} : { page: windowed.page }
|
|
106483
|
+
};
|
|
106484
|
+
});
|
|
106485
|
+
}
|
|
105774
106486
|
function makeSnapshotThenLiveStream(input) {
|
|
105775
106487
|
return unwrap$1(gen(function* () {
|
|
105776
106488
|
const liveEvents = yield* input.subscribeLiveEvents;
|
|
@@ -106017,6 +106729,9 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
|
|
|
106017
106729
|
shellRevealInFileManager: true,
|
|
106018
106730
|
shellRevealInFileManagerKind: fileManagerRevealKind
|
|
106019
106731
|
},
|
|
106732
|
+
threadResumeCompletionMarker: true,
|
|
106733
|
+
threadSnapshotPagination: true,
|
|
106734
|
+
threadDetailPageRpc: true,
|
|
106020
106735
|
observability: {
|
|
106021
106736
|
logsDirectoryPath: config.logsDir,
|
|
106022
106737
|
localTracingEnabled: true,
|
|
@@ -106114,26 +106829,28 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
|
|
|
106114
106829
|
projectionSnapshotQuery
|
|
106115
106830
|
})), flatMap$3((event) => isSome(event) ? succeed$4(event.value) : empty$5))))
|
|
106116
106831
|
})), { "rpc.aggregate": "orchestration" }),
|
|
106117
|
-
[ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread,
|
|
106118
|
-
|
|
106119
|
-
|
|
106832
|
+
[ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, succeed(makeThreadSubscriptionStream(input, {
|
|
106833
|
+
subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(map$3((events) => events.pipe(filter$2(isThreadDetailEventFor(input.threadId))))),
|
|
106834
|
+
currentSequence: orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence)),
|
|
106835
|
+
loadSnapshot: loadThreadDetailSnapshot({
|
|
106836
|
+
threadId: input.threadId,
|
|
106837
|
+
window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
|
|
106838
|
+
projectionSnapshotQuery,
|
|
106839
|
+
orchestrationEngine
|
|
106840
|
+
}),
|
|
106841
|
+
replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(filter$2(isThreadDetailEventFor(input.threadId)), mapError$1((cause) => new OrchestrationGetSnapshotError({
|
|
106842
|
+
message: `Failed to replay thread ${input.threadId}`,
|
|
106120
106843
|
cause
|
|
106121
|
-
})))
|
|
106122
|
-
|
|
106123
|
-
|
|
106124
|
-
|
|
106125
|
-
|
|
106126
|
-
|
|
106127
|
-
|
|
106128
|
-
|
|
106129
|
-
|
|
106130
|
-
|
|
106131
|
-
kind: "snapshot",
|
|
106132
|
-
snapshot: {
|
|
106133
|
-
snapshotSequence,
|
|
106134
|
-
thread: threadDetail.value
|
|
106135
|
-
}
|
|
106136
|
-
}), liveStream);
|
|
106844
|
+
})))
|
|
106845
|
+
})), { "rpc.aggregate": "orchestration" }),
|
|
106846
|
+
[ORCHESTRATION_WS_METHODS.getThreadDetailPage]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getThreadDetailPage, loadThreadDetailSnapshot({
|
|
106847
|
+
threadId: input.threadId,
|
|
106848
|
+
window: {
|
|
106849
|
+
...input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
|
|
106850
|
+
...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor }
|
|
106851
|
+
},
|
|
106852
|
+
projectionSnapshotQuery,
|
|
106853
|
+
orchestrationEngine
|
|
106137
106854
|
}), { "rpc.aggregate": "orchestration" }),
|
|
106138
106855
|
[WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
|
|
106139
106856
|
[WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(map$3((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
|
|
@@ -117598,8 +118315,8 @@ effectDiscard(runMigrations());
|
|
|
117598
118315
|
//#endregion
|
|
117599
118316
|
//#region src/persistence/Layers/Sqlite.ts
|
|
117600
118317
|
const defaultSqliteClientLoaders = {
|
|
117601
|
-
bun: () => import("./SqliteClient-
|
|
117602
|
-
node: () => import("./NodeSqliteClient-
|
|
118318
|
+
bun: () => import("./SqliteClient-BKB695RF.mjs"),
|
|
118319
|
+
node: () => import("./NodeSqliteClient-md8xd9hY.mjs")
|
|
117603
118320
|
};
|
|
117604
118321
|
const makeRuntimeSqliteLayer = fn$1("makeRuntimeSqliteLayer")(function* (config) {
|
|
117605
118322
|
const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
|
|
@@ -183040,7 +183757,7 @@ var AM = class {
|
|
|
183040
183757
|
return unlink$1(e);
|
|
183041
183758
|
}
|
|
183042
183759
|
list(e) {
|
|
183043
|
-
return readdir$
|
|
183760
|
+
return readdir$2(e);
|
|
183044
183761
|
}
|
|
183045
183762
|
append(e, t, n) {
|
|
183046
183763
|
return appendFile(e, t, {
|
|
@@ -183065,7 +183782,7 @@ var AM = class {
|
|
|
183065
183782
|
return { mtimeMs: (await stat$1(e)).mtimeMs };
|
|
183066
183783
|
}
|
|
183067
183784
|
async listEntries(e) {
|
|
183068
|
-
return (await readdir$
|
|
183785
|
+
return (await readdir$2(e, { withFileTypes: !0 })).map((n) => ({
|
|
183069
183786
|
name: n.name,
|
|
183070
183787
|
isDirectory: n.isDirectory(),
|
|
183071
183788
|
isFile: n.isFile()
|
|
@@ -190295,7 +191012,7 @@ var Vce = {
|
|
|
190295
191012
|
return lstat$1(e);
|
|
190296
191013
|
},
|
|
190297
191014
|
async readdir(e) {
|
|
190298
|
-
return readdir$
|
|
191015
|
+
return readdir$2(e, { withFileTypes: !0 });
|
|
190299
191016
|
},
|
|
190300
191017
|
async unlink(e) {
|
|
190301
191018
|
return unlink$1(e);
|
|
@@ -190352,7 +191069,7 @@ var Vce = {
|
|
|
190352
191069
|
return link$1(e, t);
|
|
190353
191070
|
},
|
|
190354
191071
|
async chmod(e, t) {
|
|
190355
|
-
return chmod(e, t);
|
|
191072
|
+
return chmod$1(e, t);
|
|
190356
191073
|
},
|
|
190357
191074
|
statSync(e) {
|
|
190358
191075
|
let n = [];
|
|
@@ -190535,7 +191252,7 @@ var Vce = {
|
|
|
190535
191252
|
function Ve() {
|
|
190536
191253
|
return Vce;
|
|
190537
191254
|
}
|
|
190538
|
-
var Us =
|
|
191255
|
+
var Us = os.homedir(), lk = os.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
|
|
190539
191256
|
let t = path.join(Us, "Library");
|
|
190540
191257
|
return {
|
|
190541
191258
|
data: path.join(t, "Application Support", e),
|
|
@@ -190871,7 +191588,7 @@ async function hme(e) {
|
|
|
190871
191588
|
await Promise.all(o.map(async (s) => {
|
|
190872
191589
|
let i;
|
|
190873
191590
|
try {
|
|
190874
|
-
i = await readdir$
|
|
191591
|
+
i = await readdir$2(s, { withFileTypes: !0 });
|
|
190875
191592
|
} catch {
|
|
190876
191593
|
return;
|
|
190877
191594
|
}
|
|
@@ -200735,12 +201452,12 @@ function qZ(e, t) {
|
|
|
200735
201452
|
}
|
|
200736
201453
|
return n;
|
|
200737
201454
|
}
|
|
200738
|
-
var os$
|
|
201455
|
+
var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
200739
201456
|
{
|
|
200740
201457
|
matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
|
|
200741
201458
|
tip: {
|
|
200742
201459
|
suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
|
|
200743
|
-
docLink: `${os$
|
|
201460
|
+
docLink: `${os$2}/iam#permission-modes`
|
|
200744
201461
|
}
|
|
200745
201462
|
},
|
|
200746
201463
|
{
|
|
@@ -200755,7 +201472,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
|
|
|
200755
201472
|
matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
|
|
200756
201473
|
tip: {
|
|
200757
201474
|
suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
|
|
200758
|
-
docLink: `${os$
|
|
201475
|
+
docLink: `${os$2}/settings#environment-variables`
|
|
200759
201476
|
}
|
|
200760
201477
|
},
|
|
200761
201478
|
{
|
|
@@ -200766,14 +201483,14 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
|
|
|
200766
201483
|
matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
|
|
200767
201484
|
tip: {
|
|
200768
201485
|
suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
|
|
200769
|
-
docLink: `${os$
|
|
201486
|
+
docLink: `${os$2}/hooks`
|
|
200770
201487
|
}
|
|
200771
201488
|
},
|
|
200772
201489
|
{
|
|
200773
201490
|
matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
|
|
200774
201491
|
tip: {
|
|
200775
201492
|
suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
|
|
200776
|
-
docLink: `${os$
|
|
201493
|
+
docLink: `${os$2}/hooks#exec-form-and-shell-form`
|
|
200777
201494
|
}
|
|
200778
201495
|
},
|
|
200779
201496
|
{
|
|
@@ -200788,7 +201505,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
|
|
|
200788
201505
|
matches: (e) => e.code === "unrecognized_keys",
|
|
200789
201506
|
tip: {
|
|
200790
201507
|
suggestion: "Check for typos or refer to the documentation for valid fields",
|
|
200791
|
-
docLink: `${os$
|
|
201508
|
+
docLink: `${os$2}/settings`
|
|
200792
201509
|
}
|
|
200793
201510
|
},
|
|
200794
201511
|
{
|
|
@@ -200803,13 +201520,13 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
|
|
|
200803
201520
|
matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
|
|
200804
201521
|
tip: {
|
|
200805
201522
|
suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
|
|
200806
|
-
docLink: `${os$
|
|
201523
|
+
docLink: `${os$2}/iam#working-directories`
|
|
200807
201524
|
}
|
|
200808
201525
|
}
|
|
200809
201526
|
], AKe = {
|
|
200810
|
-
permissions: `${os$
|
|
200811
|
-
env: `${os$
|
|
200812
|
-
hooks: `${os$
|
|
201527
|
+
permissions: `${os$2}/iam#configuring-permissions`,
|
|
201528
|
+
env: `${os$2}/settings#environment-variables`,
|
|
201529
|
+
hooks: `${os$2}/hooks`
|
|
200813
201530
|
};
|
|
200814
201531
|
function GZ(e) {
|
|
200815
201532
|
let t = kKe.find((r) => r.matches(e));
|
|
@@ -212181,6 +212898,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
|
212181
212898
|
/\bCURSOR_API_KEY\b/,
|
|
212182
212899
|
/\bOPENCODE_API_KEY\b/,
|
|
212183
212900
|
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
212901
|
+
/\bProviderValidationError\b/,
|
|
212902
|
+
/\/forge-server\/node_modules\//,
|
|
212184
212903
|
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
212185
212904
|
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
212186
212905
|
];
|
|
@@ -212190,6 +212909,12 @@ function isProviderRateOrQuotaError(message) {
|
|
|
212190
212909
|
const normalized = message.toLowerCase();
|
|
212191
212910
|
return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
|
|
212192
212911
|
}
|
|
212912
|
+
function providerLifecycleOrAccountCopy(message) {
|
|
212913
|
+
if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
|
|
212914
|
+
if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
|
|
212915
|
+
if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
|
|
212916
|
+
return null;
|
|
212917
|
+
}
|
|
212193
212918
|
/**
|
|
212194
212919
|
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
212195
212920
|
*
|
|
@@ -212202,7 +212927,8 @@ function isProviderRateOrQuotaError(message) {
|
|
|
212202
212927
|
function categorizePublicProviderError(message) {
|
|
212203
212928
|
if (!message) return null;
|
|
212204
212929
|
const m = message.toLowerCase();
|
|
212205
|
-
|
|
212930
|
+
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
212931
|
+
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
212206
212932
|
if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
|
|
212207
212933
|
if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
|
|
212208
212934
|
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
@@ -212268,60 +212994,14 @@ function hasConfiguredApexCredential(profiles) {
|
|
|
212268
212994
|
}
|
|
212269
212995
|
//#endregion
|
|
212270
212996
|
//#region src/provider/claudeCredentialDirectory.ts
|
|
212271
|
-
/**
|
|
212272
|
-
* Per-account credential directories for Claude turns.
|
|
212273
|
-
*
|
|
212274
|
-
* Claude Code keeps conversation transcripts under
|
|
212275
|
-
* `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
|
|
212276
|
-
* under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
|
|
212277
|
-
* credential file, so each Provider Account can hold its own login while every
|
|
212278
|
-
* session file stays in one place and `--resume` keeps finding its target. See
|
|
212279
|
-
* `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
|
|
212280
|
-
*
|
|
212281
|
-
* That variable is not documented. Under
|
|
212282
|
-
* `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
|
|
212283
|
-
* only allowed because absence degrades to a named behaviour that already
|
|
212284
|
-
* works: a thread with no account runs on the Environment's own login. This
|
|
212285
|
-
* module sets the variable only when the resolved directory holds a credential
|
|
212286
|
-
* the CLI can use, because a missing file, or one whose access token a refusal
|
|
212287
|
-
* has blanked, makes the CLI report logged out and a live turn would fail.
|
|
212288
|
-
*
|
|
212289
|
-
* Nothing writes these directories yet. #3049 adds the writer and keys each
|
|
212290
|
-
* directory on the account identity CMMD writes on the delivery receipt it
|
|
212291
|
-
* signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
|
|
212292
|
-
* every runtime takes the `no-account` skip.
|
|
212293
|
-
*
|
|
212294
|
-
* Free of Effect services on purpose, for the same reason as
|
|
212295
|
-
* `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
|
|
212296
|
-
* services, the browser manager and the server config, so a unit test that
|
|
212297
|
-
* imports it cannot run in a worktree. This rule decides which account pays for
|
|
212298
|
-
* a turn, so it has to stay directly testable.
|
|
212299
|
-
*/
|
|
212997
|
+
/** Per-account credentials share the existing Claude transcript directory. */
|
|
212300
212998
|
/** Repoints `.credentials.json` without moving the transcripts. */
|
|
212301
212999
|
const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
212302
213000
|
/** The one file Claude Code writes into the credential directory. */
|
|
212303
213001
|
const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
|
|
212304
|
-
/**
|
|
212305
|
-
* Runtime env var naming the Provider Account this runtime's Claude turns
|
|
212306
|
-
* belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
|
|
212307
|
-
* and each keeps its one login.
|
|
212308
|
-
*
|
|
212309
|
-
* It is a bridge that will not get a writer. #3073 settled the key in
|
|
212310
|
-
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
|
|
212311
|
-
* comes from the identity CMMD issues and writes on the delivery receipt it
|
|
212312
|
-
* signs, and this variable goes away. #3049 makes that change. The credential
|
|
212313
|
-
* generation was ruled out as a key: it rotates on every reconnect, and CMMD
|
|
212314
|
-
* uses it as the primary account's id, so keying on it would strand every
|
|
212315
|
-
* thread pinned to that account on a directory that stops receiving deliveries.
|
|
212316
|
-
*
|
|
212317
|
-
* Tests reach the `credentialDirectory` parameter on
|
|
212318
|
-
* `ClaudeAdapter.buildClaudeQueryEnv` directly.
|
|
212319
|
-
*/
|
|
212320
|
-
const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
|
|
212321
213002
|
/** Directory under the server's base directory that holds one directory per account. */
|
|
212322
213003
|
const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
|
|
212323
213004
|
const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
|
|
212324
|
-
const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
|
|
212325
213005
|
const DIRECTORY_NAME_HASH_LENGTH = 32;
|
|
212326
213006
|
/**
|
|
212327
213007
|
* Directory name for an account. A hash, so no email or display name reaches a
|
|
@@ -212330,11 +213010,12 @@ const DIRECTORY_NAME_HASH_LENGTH = 32;
|
|
|
212330
213010
|
function claudeCredentialDirectoryName(accountIdentity) {
|
|
212331
213011
|
return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
|
|
212332
213012
|
}
|
|
212333
|
-
/**
|
|
212334
|
-
function
|
|
212335
|
-
|
|
212336
|
-
|
|
212337
|
-
|
|
213013
|
+
/** Only a matching CMMD delivery can select a credential directory. */
|
|
213014
|
+
function resolveSelectedClaudeAccountIdentity(selectedAccountId, binding) {
|
|
213015
|
+
if (selectedAccountId === void 0) return null;
|
|
213016
|
+
const identity = binding?.identity;
|
|
213017
|
+
if (identity?.kind !== "credential_generation" || identity.provider !== "claude" || identity.providerAccountIdentity !== selectedAccountId) throw new Error("The selected Claude account has no matching credential delivery. Reconnect that account and try again.");
|
|
213018
|
+
return identity.providerAccountIdentity;
|
|
212338
213019
|
}
|
|
212339
213020
|
/**
|
|
212340
213021
|
* Where per-account credential directories live: `claude-credentials` under the
|
|
@@ -212361,29 +213042,19 @@ function resolveClaudeCredentialDirectory(input) {
|
|
|
212361
213042
|
if (!root) return null;
|
|
212362
213043
|
return join(root, claudeCredentialDirectoryName(input.accountIdentity));
|
|
212363
213044
|
}
|
|
212364
|
-
/**
|
|
212365
|
-
* Decide whether this turn may point at a per-account credential directory.
|
|
212366
|
-
*
|
|
212367
|
-
* The cheap checks run first and the capability probe runs last, so a runtime
|
|
212368
|
-
* with no usable credential never spawns the CLI. The probe's error channel is
|
|
212369
|
-
* empty, so nothing here catches.
|
|
212370
|
-
*
|
|
212371
|
-
* A skip is not a fallback onto the default account. With no account the turn
|
|
212372
|
-
* runs on the Environment's own login, unchanged. A thread pinned to an account
|
|
212373
|
-
* whose credential is missing is refused by name under
|
|
212374
|
-
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
|
|
212375
|
-
* `no-usable-credential`, and `capability-not-supported` for a pinned account,
|
|
212376
|
-
* into those refusals. Until #3049 nothing names an account, so `no-account` is
|
|
212377
|
-
* the only skip a runtime sees.
|
|
212378
|
-
*/
|
|
213045
|
+
/** Refuse selected accounts that cannot use their own credential directory. */
|
|
212379
213046
|
function decideClaudeCredentialDirectory(input) {
|
|
212380
213047
|
return gen(function* () {
|
|
212381
213048
|
if (!input.accountIdentity) return skip("no-account");
|
|
212382
|
-
if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return
|
|
213049
|
+
if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return refuse("operator-override");
|
|
212383
213050
|
const directory = resolveClaudeCredentialDirectory(input);
|
|
212384
|
-
if (!directory) return
|
|
212385
|
-
if (!holdsUsableCredential(directory)) return
|
|
212386
|
-
|
|
213051
|
+
if (!directory) return refuse("no-root");
|
|
213052
|
+
if (!holdsUsableCredential(directory)) return refuse("no-usable-credential");
|
|
213053
|
+
if ((yield* input.probeSupport) === "supported") return set(directory);
|
|
213054
|
+
return input.primaryCredentialMatchesSelected === true ? {
|
|
213055
|
+
outcome: "skipped",
|
|
213056
|
+
reason: "same-primary-account"
|
|
213057
|
+
} : refuse("capability-not-supported");
|
|
212387
213058
|
});
|
|
212388
213059
|
}
|
|
212389
213060
|
/**
|
|
@@ -212422,6 +213093,12 @@ function skip(reason) {
|
|
|
212422
213093
|
reason
|
|
212423
213094
|
};
|
|
212424
213095
|
}
|
|
213096
|
+
function refuse(reason) {
|
|
213097
|
+
return {
|
|
213098
|
+
outcome: "refused",
|
|
213099
|
+
reason
|
|
213100
|
+
};
|
|
213101
|
+
}
|
|
212425
213102
|
function claudeConfigDirectory(env) {
|
|
212426
213103
|
const configured = env.CLAUDE_CONFIG_DIR?.trim();
|
|
212427
213104
|
if (configured) return configured;
|
|
@@ -212431,6 +213108,24 @@ function claudeConfigDirectory(env) {
|
|
|
212431
213108
|
return null;
|
|
212432
213109
|
}
|
|
212433
213110
|
}
|
|
213111
|
+
/**
|
|
213112
|
+
* The fallback is allowed only when the primary on-disk credential is
|
|
213113
|
+
* byte-identical to what CMMD prepared for the selected account. A ticket's
|
|
213114
|
+
* `credentialRevisionHash` cannot express that: it hashes the encrypted row
|
|
213115
|
+
* CMMD stores, not the decrypted bytes this process ever sees. Comparing
|
|
213116
|
+
* files directly is the only check that means what the fallback needs.
|
|
213117
|
+
*/
|
|
213118
|
+
function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
|
|
213119
|
+
const directory = claudeConfigDirectory(env);
|
|
213120
|
+
if (!directory) return false;
|
|
213121
|
+
try {
|
|
213122
|
+
const primary = readFileSync(join(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
|
|
213123
|
+
const prepared = readFileSync(join(preparedDirectory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
|
|
213124
|
+
return hasAccessToken(JSON.parse(primary)) && createHash("sha256").update(primary).digest("hex") === createHash("sha256").update(prepared).digest("hex");
|
|
213125
|
+
} catch {
|
|
213126
|
+
return false;
|
|
213127
|
+
}
|
|
213128
|
+
}
|
|
212434
213129
|
//#endregion
|
|
212435
213130
|
//#region src/provider/personalClaudeInVm.ts
|
|
212436
213131
|
/**
|
|
@@ -212563,13 +213258,106 @@ function withoutInheritedAnthropicCredentials(env, options) {
|
|
|
212563
213258
|
* marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
|
|
212564
213259
|
* has the marker and no env var until it restarts, drops the override too.
|
|
212565
213260
|
*/
|
|
212566
|
-
function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
|
|
213261
|
+
function claudeSubscriptionChildEnv(processEnv, credentialDirectory, selectedSubscription = false) {
|
|
212567
213262
|
return {
|
|
212568
|
-
...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
|
|
213263
|
+
...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: selectedSubscription || credentialDirectory !== null || personalClaudeInVmEnabled(processEnv) }),
|
|
212569
213264
|
...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
|
|
212570
213265
|
};
|
|
212571
213266
|
}
|
|
212572
213267
|
//#endregion
|
|
213268
|
+
//#region src/provider/claudeSelectedCredentialStore.ts
|
|
213269
|
+
async function ensurePrivateDirectory(path) {
|
|
213270
|
+
await mkdir(path, {
|
|
213271
|
+
recursive: true,
|
|
213272
|
+
mode: 448
|
|
213273
|
+
});
|
|
213274
|
+
const info = await lstat(path);
|
|
213275
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe credential directory.");
|
|
213276
|
+
await chmod(path, 448);
|
|
213277
|
+
}
|
|
213278
|
+
async function makeCapacity(root, target, active, maximum) {
|
|
213279
|
+
const entries = (await readdir$1(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && /^account-[a-f0-9]{32}$/.test(entry.name));
|
|
213280
|
+
if (entries.some((entry) => join(root, entry.name) === target)) return;
|
|
213281
|
+
const candidates = await Promise.all(entries.filter((entry) => !active.has(join(root, entry.name))).map(async (entry) => ({
|
|
213282
|
+
path: join(root, entry.name),
|
|
213283
|
+
modified: (await lstat(join(root, entry.name))).mtimeMs
|
|
213284
|
+
})));
|
|
213285
|
+
candidates.sort((a, b) => a.modified - b.modified);
|
|
213286
|
+
const required = entries.length - maximum + 1;
|
|
213287
|
+
if (required > candidates.length) throw new Error("All Claude credential slots are active. Stop a session before choosing another account.");
|
|
213288
|
+
for (const candidate of candidates.slice(0, Math.max(0, required))) await rm(candidate.path, { recursive: true });
|
|
213289
|
+
}
|
|
213290
|
+
function createClaudeSelectedCredentialStore(baseDir, options = {}) {
|
|
213291
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
213292
|
+
const maximum = options.maxAccounts ?? 8;
|
|
213293
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1) throw new Error("Invalid credential capacity.");
|
|
213294
|
+
let queue = Promise.resolve();
|
|
213295
|
+
async function install(threadId, acquired) {
|
|
213296
|
+
if (acquired.claims.threadId !== threadId) throw new Error("Credential thread mismatch.");
|
|
213297
|
+
const directory = resolveClaudeCredentialDirectory({
|
|
213298
|
+
env: process.env,
|
|
213299
|
+
baseDir,
|
|
213300
|
+
accountIdentity: acquired.claims.providerAccountId
|
|
213301
|
+
});
|
|
213302
|
+
if (!directory) throw new Error("Unsafe credential directory.");
|
|
213303
|
+
const binding = {
|
|
213304
|
+
runtimeId: acquired.claims.runtimeId,
|
|
213305
|
+
machineId: acquired.claims.machineId,
|
|
213306
|
+
credentialEpoch: acquired.claims.credentialEpoch,
|
|
213307
|
+
identity: {
|
|
213308
|
+
kind: "credential_generation",
|
|
213309
|
+
provider: "claude",
|
|
213310
|
+
credentialGenerationId: acquired.claims.generationId,
|
|
213311
|
+
providerAccountIdentity: acquired.claims.providerAccountId
|
|
213312
|
+
}
|
|
213313
|
+
};
|
|
213314
|
+
for (const prepared of sessions.values()) if (prepared.directory === directory && (!providerDeliveryBindingsEqual(prepared.binding, binding) || prepared.credentialRevisionHash !== acquired.claims.credentialRevisionHash)) throw new Error("The Claude account has an active session. Stop it before replacing its credentials.");
|
|
213315
|
+
const root = dirname(directory);
|
|
213316
|
+
await ensurePrivateDirectory(root);
|
|
213317
|
+
await makeCapacity(root, directory, new Set([...sessions.values()].map((entry) => entry.directory)), maximum);
|
|
213318
|
+
await ensurePrivateDirectory(directory);
|
|
213319
|
+
const temporary = join(directory, `.credential-${randomUUID()}.tmp`);
|
|
213320
|
+
try {
|
|
213321
|
+
await writeFile(temporary, acquired.credentialJson, {
|
|
213322
|
+
mode: 384,
|
|
213323
|
+
flag: "wx"
|
|
213324
|
+
});
|
|
213325
|
+
await rename(temporary, join(directory, ".credentials.json"));
|
|
213326
|
+
} finally {
|
|
213327
|
+
await rm(temporary, { force: true });
|
|
213328
|
+
}
|
|
213329
|
+
sessions.set(threadId, {
|
|
213330
|
+
directory,
|
|
213331
|
+
binding,
|
|
213332
|
+
accountKind: acquired.claims.accountKind,
|
|
213333
|
+
credentialRevisionHash: acquired.claims.credentialRevisionHash
|
|
213334
|
+
});
|
|
213335
|
+
return binding;
|
|
213336
|
+
}
|
|
213337
|
+
return {
|
|
213338
|
+
prepare(threadId, acquired) {
|
|
213339
|
+
const operation = queue.then(() => install(threadId, acquired));
|
|
213340
|
+
queue = operation.then(() => void 0, () => void 0);
|
|
213341
|
+
return operation;
|
|
213342
|
+
},
|
|
213343
|
+
read(threadId, binding) {
|
|
213344
|
+
const prepared = sessions.get(threadId);
|
|
213345
|
+
return prepared && providerDeliveryBindingsEqual(prepared.binding, binding) ? prepared : null;
|
|
213346
|
+
},
|
|
213347
|
+
release(threadId) {
|
|
213348
|
+
sessions.delete(threadId);
|
|
213349
|
+
}
|
|
213350
|
+
};
|
|
213351
|
+
}
|
|
213352
|
+
const stores = /* @__PURE__ */ new Map();
|
|
213353
|
+
function selectedClaudeCredentialStore(baseDir) {
|
|
213354
|
+
const existing = stores.get(baseDir);
|
|
213355
|
+
if (existing) return existing;
|
|
213356
|
+
const store = createClaudeSelectedCredentialStore(baseDir);
|
|
213357
|
+
stores.set(baseDir, store);
|
|
213358
|
+
return store;
|
|
213359
|
+
}
|
|
213360
|
+
//#endregion
|
|
212573
213361
|
//#region src/provider/claudeCredentialDirectoryProbe.ts
|
|
212574
213362
|
/**
|
|
212575
213363
|
* Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
|
|
@@ -214611,7 +215399,8 @@ function claudeForgeAttributionPrompt() {
|
|
|
214611
215399
|
* turn, an API key is, so pointing the variable there would add a second source
|
|
214612
215400
|
* of truth that a later reader has to rule out.
|
|
214613
215401
|
*/
|
|
214614
|
-
function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
|
|
215402
|
+
function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory, selectedSubscription = false) {
|
|
215403
|
+
if (selectedSubscription) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory, true);
|
|
214615
215404
|
const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
|
|
214616
215405
|
const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
|
|
214617
215406
|
if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
|
|
@@ -216524,6 +217313,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
216524
217313
|
if (streamFiber && streamFiber.pollUnsafe() === void 0) yield* interrupt$4(streamFiber);
|
|
216525
217314
|
try {
|
|
216526
217315
|
context.query.close();
|
|
217316
|
+
selectedClaudeCredentialStore(serverConfig.baseDir).release(context.session.threadId);
|
|
216527
217317
|
} catch (cause) {
|
|
216528
217318
|
yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
|
|
216529
217319
|
}
|
|
@@ -216835,6 +217625,43 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
216835
217625
|
sessionId: existingResumeSessionId ?? newSessionId
|
|
216836
217626
|
};
|
|
216837
217627
|
});
|
|
217628
|
+
const resolveSelectedCredentialDirectory = fn$1("resolveSelectedCredentialDirectory")(function* (input, resumeState, claudeBinaryPath) {
|
|
217629
|
+
const requestedAccountId = input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.providerAccountId : void 0;
|
|
217630
|
+
const selectedAccountIdentity = yield* try_({
|
|
217631
|
+
try: () => resolveSelectedClaudeAccountIdentity(requestedAccountId, resumeState?.providerDeliveryBinding),
|
|
217632
|
+
catch: () => new ProviderAdapterValidationError({
|
|
217633
|
+
provider: PROVIDER$11,
|
|
217634
|
+
operation: "startSession",
|
|
217635
|
+
issue: `The selected Claude account ${requestedAccountId} has no matching credential delivery. Reconnect that account and try again.`
|
|
217636
|
+
})
|
|
217637
|
+
});
|
|
217638
|
+
const preparedCredential = resumeState?.providerDeliveryBinding ? selectedClaudeCredentialStore(serverConfig.baseDir).read(input.threadId, resumeState.providerDeliveryBinding) : null;
|
|
217639
|
+
if (selectedAccountIdentity !== null && preparedCredential === null) return yield* new ProviderAdapterValidationError({
|
|
217640
|
+
provider: PROVIDER$11,
|
|
217641
|
+
operation: "startSession",
|
|
217642
|
+
issue: `The selected Claude account ${selectedAccountIdentity} has no prepared credential. Try again to reconnect it.`
|
|
217643
|
+
});
|
|
217644
|
+
const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
|
|
217645
|
+
env: process.env,
|
|
217646
|
+
baseDir: serverConfig.baseDir,
|
|
217647
|
+
accountIdentity: selectedAccountIdentity,
|
|
217648
|
+
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
|
|
217649
|
+
primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
|
|
217650
|
+
});
|
|
217651
|
+
if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
|
|
217652
|
+
provider: PROVIDER$11,
|
|
217653
|
+
operation: "startSession",
|
|
217654
|
+
issue: `The selected Claude account ${selectedAccountIdentity} cannot run on this environment: ${credentialDirectoryDecision.reason}. Reconnect the account or choose another environment.`
|
|
217655
|
+
});
|
|
217656
|
+
if (credentialDirectoryDecision.outcome === "skipped") {
|
|
217657
|
+
const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
|
|
217658
|
+
yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
|
|
217659
|
+
}
|
|
217660
|
+
return {
|
|
217661
|
+
selectedAccountIdentity,
|
|
217662
|
+
claudeCredentialDirectory: credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null
|
|
217663
|
+
};
|
|
217664
|
+
});
|
|
216838
217665
|
const startSession = fn$1("startSession")(function* (input) {
|
|
216839
217666
|
const sessionProvider = yield* validateClaudeStartSessionProvider(input.provider);
|
|
216840
217667
|
const startedAt = yield* nowIso;
|
|
@@ -216895,19 +217722,14 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
216895
217722
|
})));
|
|
216896
217723
|
const claudeSettings = serverSettings.providers.claudeAgent;
|
|
216897
217724
|
const claudeBinaryPath = claudeSettings.binaryPath;
|
|
216898
|
-
const
|
|
216899
|
-
env: process.env,
|
|
216900
|
-
baseDir: serverConfig.baseDir,
|
|
216901
|
-
accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
|
|
216902
|
-
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
|
|
216903
|
-
});
|
|
216904
|
-
if (credentialDirectoryDecision.outcome === "skipped") {
|
|
216905
|
-
const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
|
|
216906
|
-
yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
|
|
216907
|
-
}
|
|
216908
|
-
const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
|
|
217725
|
+
const { selectedAccountIdentity, claudeCredentialDirectory } = yield* resolveSelectedCredentialDirectory(input, resumeState, claudeBinaryPath);
|
|
216909
217726
|
const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
|
|
216910
|
-
|
|
217727
|
+
if (selectedAccountIdentity !== null && modelConfig.endpointProfile !== void 0) return yield* new ProviderAdapterValidationError({
|
|
217728
|
+
provider: PROVIDER$11,
|
|
217729
|
+
operation: "startSession",
|
|
217730
|
+
issue: "The selected Claude account cannot be used with a custom API endpoint. Choose a subscription model."
|
|
217731
|
+
});
|
|
217732
|
+
const providerProxyCredential = selectedAccountIdentity !== null ? null : yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
|
|
216911
217733
|
const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
|
|
216912
217734
|
const settings = claudeQuerySettings(modelConfig.thinking, modelConfig.fastMode);
|
|
216913
217735
|
const workspacePreflight = input.cwd ? formatWorkspaceEnvironmentPreflightForAgent(runWorkspaceEnvironmentPreflight({ cwd: input.cwd })) : void 0;
|
|
@@ -216930,7 +217752,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
216930
217752
|
existingResumeSessionId,
|
|
216931
217753
|
newSessionId,
|
|
216932
217754
|
canUseTool,
|
|
216933
|
-
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
|
|
217755
|
+
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory, selectedAccountIdentity !== null),
|
|
216934
217756
|
browserManager,
|
|
216935
217757
|
threadId,
|
|
216936
217758
|
browserToolEnvironmentId,
|
|
@@ -216982,7 +217804,9 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
216982
217804
|
});
|
|
216983
217805
|
attachClaudeStreamFiber(context$4, runFork);
|
|
216984
217806
|
return { ...session };
|
|
216985
|
-
})
|
|
217807
|
+
}, (effect, input) => effect.pipe(onExit((exit) => sync$1(() => {
|
|
217808
|
+
if (exit._tag === "Failure") selectedClaudeCredentialStore(serverConfig.baseDir).release(input.threadId);
|
|
217809
|
+
}))));
|
|
216986
217810
|
const sendTurn = fn$1("sendTurn")(function* (input) {
|
|
216987
217811
|
const context = yield* requireSession(input.threadId);
|
|
216988
217812
|
const modelSelection = input.modelSelection?.provider === "claudeAgent" || input.modelSelection?.provider === "apex" || input.modelSelection?.provider === "cmmd" ? input.modelSelection : void 0;
|
|
@@ -231264,6 +232088,9 @@ const ProviderRollbackConversationInput = Struct({
|
|
|
231264
232088
|
threadId: ThreadId,
|
|
231265
232089
|
numTurns: NonNegativeInt
|
|
231266
232090
|
});
|
|
232091
|
+
function selectedAccountGuardFields(providerAccountId) {
|
|
232092
|
+
return providerAccountId === void 0 ? {} : { providerAccountId };
|
|
232093
|
+
}
|
|
231267
232094
|
function cursorRecord(value) {
|
|
231268
232095
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
|
|
231269
232096
|
}
|
|
@@ -231356,9 +232183,13 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
|
|
|
231356
232183
|
const runtimeEventPubSub = yield* unbounded$1();
|
|
231357
232184
|
const assertProviderExecutable = options?.assertProviderExecutable ?? (() => succeed(null));
|
|
231358
232185
|
const assertCurrentProviderDelivery = fn$1("assertCurrentProviderDelivery")(function* (input) {
|
|
231359
|
-
const currentBinding = yield* assertProviderExecutable(input);
|
|
231360
|
-
if (!currentBinding) return;
|
|
231361
232186
|
const persistedBinding = getOrUndefined(yield* directory.getBinding(input.threadId));
|
|
232187
|
+
const providerAccountId = readPersistedProviderAccountId(persistedBinding?.runtimePayload);
|
|
232188
|
+
const currentBinding = yield* assertProviderExecutable({
|
|
232189
|
+
...input,
|
|
232190
|
+
...selectedAccountGuardFields(providerAccountId)
|
|
232191
|
+
});
|
|
232192
|
+
if (!currentBinding) return;
|
|
231362
232193
|
if (!persistedBinding || persistedBinding.provider !== input.provider) return yield* toValidationError(input.operation, `Cannot verify the delivered provider identity for thread '${input.threadId}'.`);
|
|
231363
232194
|
yield* bindProviderDeliveryToResumeCursor({
|
|
231364
232195
|
operation: input.operation,
|
|
@@ -231413,10 +232244,12 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
|
|
|
231413
232244
|
if (!hasResumeCursor) return yield* toValidationError(input.operation, `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`);
|
|
231414
232245
|
const persistedCwd = readPersistedCwd(input.binding.runtimePayload);
|
|
231415
232246
|
const persistedModelSelection = readPersistedModelSelection(input.binding.runtimePayload);
|
|
232247
|
+
const persistedProviderAccountId = readProviderAccountIdFromModelSelection(persistedModelSelection);
|
|
231416
232248
|
const currentDeliveryBinding = yield* assertProviderExecutable({
|
|
231417
232249
|
threadId: input.binding.threadId,
|
|
231418
232250
|
provider: input.binding.provider,
|
|
231419
|
-
operation: input.operation
|
|
232251
|
+
operation: input.operation,
|
|
232252
|
+
...selectedAccountGuardFields(persistedProviderAccountId)
|
|
231420
232253
|
});
|
|
231421
232254
|
const boundResumeCursor = yield* bindProviderDeliveryToResumeCursor({
|
|
231422
232255
|
operation: input.operation,
|
|
@@ -231541,7 +232374,8 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
|
|
|
231541
232374
|
currentBinding: yield* assertProviderExecutable({
|
|
231542
232375
|
threadId,
|
|
231543
232376
|
provider: effectiveProvider,
|
|
231544
|
-
operation: "ProviderService.startSession"
|
|
232377
|
+
operation: "ProviderService.startSession",
|
|
232378
|
+
...selectedAccountGuardFields(requestedAccountId)
|
|
231545
232379
|
})
|
|
231546
232380
|
});
|
|
231547
232381
|
const session = yield* adapter.startSession({
|
|
@@ -231607,6 +232441,8 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
|
|
|
231607
232441
|
allowRecovery: true
|
|
231608
232442
|
});
|
|
231609
232443
|
const persistedBinding = getOrUndefined(yield* directory.getBinding(input.threadId));
|
|
232444
|
+
const persistedModelSelection = readPersistedModelSelection(persistedBinding?.runtimePayload);
|
|
232445
|
+
const nextModelSelection = input.modelSelection ?? persistedModelSelection;
|
|
231610
232446
|
yield* assertProviderAccountBoundary({
|
|
231611
232447
|
operation: "ProviderService.sendTurn",
|
|
231612
232448
|
threadId: input.threadId,
|
|
@@ -231631,7 +232467,7 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
|
|
|
231631
232467
|
status: "running",
|
|
231632
232468
|
...turn.resumeCursor !== void 0 ? { resumeCursor: turn.resumeCursor } : {},
|
|
231633
232469
|
runtimePayload: {
|
|
231634
|
-
...
|
|
232470
|
+
...nextModelSelection !== void 0 ? { modelSelection: nextModelSelection } : {},
|
|
231635
232471
|
activeTurnId: turn.turnId,
|
|
231636
232472
|
lastRuntimeEvent: "provider.sendTurn",
|
|
231637
232473
|
lastRuntimeEventAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -231887,6 +232723,201 @@ function makeProviderServiceLive(options) {
|
|
|
231887
232723
|
return effect(ProviderService, makeProviderService(options));
|
|
231888
232724
|
}
|
|
231889
232725
|
//#endregion
|
|
232726
|
+
//#region src/runtimeAuth/cmmdClaudeSelectedTicket.ts
|
|
232727
|
+
const ACCOUNT = /^pai_v1_[A-Za-z0-9_-]{43}$/;
|
|
232728
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
232729
|
+
const REVISION_HASH = /^[a-f0-9]{64}$/;
|
|
232730
|
+
function hasValidTicketLifetime(claims, payload) {
|
|
232731
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
232732
|
+
const issuedAt = payload.iat;
|
|
232733
|
+
const notBefore = payload.nbf;
|
|
232734
|
+
return typeof issuedAt === "number" && Number.isSafeInteger(issuedAt) && issuedAt <= now + 5 && typeof notBefore === "number" && Number.isSafeInteger(notBefore) && notBefore <= now && Number.isSafeInteger(claims.expiresAtSeconds) && claims.expiresAtSeconds - issuedAt <= 900;
|
|
232735
|
+
}
|
|
232736
|
+
function hasSelectedCredentialScope(claims) {
|
|
232737
|
+
return claims.scope.length === 1 && claims.scope[0] === "claude-credential-read";
|
|
232738
|
+
}
|
|
232739
|
+
function hasExpectedSelectedBinding(payload, expected) {
|
|
232740
|
+
return payload.provider_account_id === expected.providerAccountId && ACCOUNT.test(expected.providerAccountId) && payload.thread_id === expected.threadId && payload.operation_id === expected.operationId && UUID.test(expected.operationId);
|
|
232741
|
+
}
|
|
232742
|
+
function selectedCredentialClaims(payload) {
|
|
232743
|
+
const jti = payload.jti;
|
|
232744
|
+
const generationId = payload.credential_generation_id;
|
|
232745
|
+
const credentialRevisionHash = payload.credential_revision_hash;
|
|
232746
|
+
const accountKind = payload.account_kind;
|
|
232747
|
+
if (typeof jti !== "string" || !jti.trim() || typeof generationId !== "string" || !UUID.test(generationId) || typeof credentialRevisionHash !== "string" || !REVISION_HASH.test(credentialRevisionHash) || accountKind !== "primary" && accountKind !== "linked") return null;
|
|
232748
|
+
return {
|
|
232749
|
+
jti,
|
|
232750
|
+
generationId,
|
|
232751
|
+
credentialRevisionHash,
|
|
232752
|
+
accountKind
|
|
232753
|
+
};
|
|
232754
|
+
}
|
|
232755
|
+
function verifyCmmdClaudeSelectedTicket(token, expected) {
|
|
232756
|
+
const verified = verifyCmmdScopedRuntimePayload(token, expected, "forge-claude-credential-read");
|
|
232757
|
+
if (!verified) return null;
|
|
232758
|
+
const { claims, payload } = verified;
|
|
232759
|
+
if (!hasValidTicketLifetime(claims, payload)) return null;
|
|
232760
|
+
if (!hasSelectedCredentialScope(claims)) return null;
|
|
232761
|
+
if (!hasExpectedSelectedBinding(payload, expected)) return null;
|
|
232762
|
+
const selected = selectedCredentialClaims(payload);
|
|
232763
|
+
if (!selected) return null;
|
|
232764
|
+
return {
|
|
232765
|
+
...claims,
|
|
232766
|
+
jti: selected.jti,
|
|
232767
|
+
providerAccountId: expected.providerAccountId,
|
|
232768
|
+
generationId: selected.generationId,
|
|
232769
|
+
credentialRevisionHash: selected.credentialRevisionHash,
|
|
232770
|
+
accountKind: selected.accountKind,
|
|
232771
|
+
threadId: expected.threadId,
|
|
232772
|
+
operationId: expected.operationId
|
|
232773
|
+
};
|
|
232774
|
+
}
|
|
232775
|
+
//#endregion
|
|
232776
|
+
//#region src/provider/claudeSelectedCredentialAcquisition.ts
|
|
232777
|
+
function readToken(response) {
|
|
232778
|
+
if (typeof response !== "object" || response === null || !("data" in response)) return null;
|
|
232779
|
+
const data = response.data;
|
|
232780
|
+
return typeof data === "object" && data !== null && "token" in data && typeof data.token === "string" ? data.token : null;
|
|
232781
|
+
}
|
|
232782
|
+
async function acquireSelectedClaudeCredential(expected, transport) {
|
|
232783
|
+
try {
|
|
232784
|
+
const token = readToken(await transport.requestTicket());
|
|
232785
|
+
const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
|
|
232786
|
+
if (!token || !claims) throw new Error("invalid_ticket");
|
|
232787
|
+
const credentialJson = await transport.readCredential(token);
|
|
232788
|
+
if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
|
|
232789
|
+
return {
|
|
232790
|
+
claims,
|
|
232791
|
+
credentialJson
|
|
232792
|
+
};
|
|
232793
|
+
} catch {
|
|
232794
|
+
throw new Error("The selected Claude account could not be prepared. Reconnect the account and try again.");
|
|
232795
|
+
}
|
|
232796
|
+
}
|
|
232797
|
+
//#endregion
|
|
232798
|
+
//#region src/provider/claudeSelectedCredentialPreparer.ts
|
|
232799
|
+
const REQUEST_TIMEOUT_MS = 1e4;
|
|
232800
|
+
const OWNER_TICKET_PATH = "/api/forge/context/providers/claude/runtime-read-ticket";
|
|
232801
|
+
const CREDENTIAL_READ_PATH = "/api/forge/context/auth-runtime-claude-credential";
|
|
232802
|
+
function isRuntimeTicketSession$1(sessionId) {
|
|
232803
|
+
return String(sessionId).startsWith("cmmd-runtime:");
|
|
232804
|
+
}
|
|
232805
|
+
function nonEmptyConfigString(value) {
|
|
232806
|
+
return value?.trim() || null;
|
|
232807
|
+
}
|
|
232808
|
+
function selectedRuntimeStrings(config) {
|
|
232809
|
+
const values = [
|
|
232810
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeId),
|
|
232811
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeMachineId),
|
|
232812
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeOrgId),
|
|
232813
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeUserId),
|
|
232814
|
+
nonEmptyConfigString(config.forgeCmmdRuntimePublicKey)
|
|
232815
|
+
];
|
|
232816
|
+
if (values.includes(null)) return null;
|
|
232817
|
+
return {
|
|
232818
|
+
runtimeId: values[0],
|
|
232819
|
+
machineId: values[1],
|
|
232820
|
+
orgId: values[2],
|
|
232821
|
+
userId: values[3],
|
|
232822
|
+
publicKeyPem: values[4]
|
|
232823
|
+
};
|
|
232824
|
+
}
|
|
232825
|
+
function selectedCredentialEpoch(config) {
|
|
232826
|
+
const credentialEpoch = config.forgeCmmdRuntimeCredentialEpoch;
|
|
232827
|
+
return typeof credentialEpoch === "number" && Number.isSafeInteger(credentialEpoch) && credentialEpoch >= 0 ? credentialEpoch : null;
|
|
232828
|
+
}
|
|
232829
|
+
function selectedConfig(config) {
|
|
232830
|
+
if (config.forgeCmmdRuntimeMode !== true) return null;
|
|
232831
|
+
const strings = selectedRuntimeStrings(config);
|
|
232832
|
+
const credentialEpoch = selectedCredentialEpoch(config);
|
|
232833
|
+
if (strings === null || credentialEpoch === null) return null;
|
|
232834
|
+
return {
|
|
232835
|
+
...strings,
|
|
232836
|
+
credentialEpoch
|
|
232837
|
+
};
|
|
232838
|
+
}
|
|
232839
|
+
async function boundedRuntimeTicketRequest(grant, runtimeId, request) {
|
|
232840
|
+
const response = await fetch(resolveCommandAppEndpoint(`/api/forge/runtimes/${encodeURIComponent(runtimeId)}/provider/claude/read-ticket`), {
|
|
232841
|
+
method: "POST",
|
|
232842
|
+
headers: {
|
|
232843
|
+
authorization: `Bearer ${grant}`,
|
|
232844
|
+
"content-type": "application/json"
|
|
232845
|
+
},
|
|
232846
|
+
body: JSON.stringify(request),
|
|
232847
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
232848
|
+
});
|
|
232849
|
+
if (!response.ok) throw new Error("selected_ticket_unavailable");
|
|
232850
|
+
return await response.json();
|
|
232851
|
+
}
|
|
232852
|
+
async function boundedCredentialRead(token) {
|
|
232853
|
+
const response = await fetch(resolveCommandAppEndpoint(CREDENTIAL_READ_PATH), {
|
|
232854
|
+
headers: {
|
|
232855
|
+
authorization: `Bearer ${token}`,
|
|
232856
|
+
accept: "application/json"
|
|
232857
|
+
},
|
|
232858
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
232859
|
+
});
|
|
232860
|
+
if (!response.ok) throw new Error("selected_credential_unavailable");
|
|
232861
|
+
return await response.text();
|
|
232862
|
+
}
|
|
232863
|
+
function missingAuthority() {
|
|
232864
|
+
throw new Error("selected_authority_unavailable");
|
|
232865
|
+
}
|
|
232866
|
+
/**
|
|
232867
|
+
* Acquires one signed, account-scoped Claude credential for the thread that
|
|
232868
|
+
* selected it. Owner and runtime-ticket sessions use different CMMD lanes.
|
|
232869
|
+
*/
|
|
232870
|
+
function makeClaudeSelectedCredentialPreparer(config, dependencies) {
|
|
232871
|
+
const readRuntimeAuthority = dependencies.readRuntimeAuthority ?? readRuntimeBrokerAuthority;
|
|
232872
|
+
const requestOwnerTicket = dependencies.requestOwnerTicket ?? ((jwt, request) => proxyPost(jwt, OWNER_TICKET_PATH, request, void 0, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }));
|
|
232873
|
+
const requestRuntimeTicket = dependencies.requestRuntimeTicket ?? boundedRuntimeTicketRequest;
|
|
232874
|
+
const readCredential = dependencies.readCredential ?? boundedCredentialRead;
|
|
232875
|
+
const prepare = dependencies.prepare ?? ((threadId, acquired) => {
|
|
232876
|
+
if (!config.baseDir) return Promise.reject(/* @__PURE__ */ new Error("selected_store_unavailable"));
|
|
232877
|
+
return selectedClaudeCredentialStore(config.baseDir).prepare(threadId, acquired);
|
|
232878
|
+
});
|
|
232879
|
+
const createOperationId = dependencies.createOperationId ?? randomUUID;
|
|
232880
|
+
return async (input) => {
|
|
232881
|
+
const sealed = selectedConfig(config);
|
|
232882
|
+
const sessionId = input.executionSessionId ?? dependencies.readSessionId(input.threadId);
|
|
232883
|
+
if (sealed === null || sessionId === null) return missingAuthority();
|
|
232884
|
+
const operationId = createOperationId();
|
|
232885
|
+
const request = {
|
|
232886
|
+
providerAccountId: input.providerAccountId,
|
|
232887
|
+
threadId: String(input.threadId),
|
|
232888
|
+
operationId
|
|
232889
|
+
};
|
|
232890
|
+
const acquired = await acquireSelectedClaudeCredential({
|
|
232891
|
+
...sealed,
|
|
232892
|
+
providerAccountId: input.providerAccountId,
|
|
232893
|
+
threadId: String(input.threadId),
|
|
232894
|
+
operationId
|
|
232895
|
+
}, {
|
|
232896
|
+
requestTicket: async () => {
|
|
232897
|
+
if (isRuntimeTicketSession$1(sessionId)) {
|
|
232898
|
+
const grant = readRuntimeAuthority({
|
|
232899
|
+
sessionId,
|
|
232900
|
+
runtimeId: sealed.runtimeId,
|
|
232901
|
+
machineId: sealed.machineId,
|
|
232902
|
+
credentialEpoch: sealed.credentialEpoch,
|
|
232903
|
+
requiredScope: "provider"
|
|
232904
|
+
});
|
|
232905
|
+
if (grant === null) return missingAuthority();
|
|
232906
|
+
return { data: await requestRuntimeTicket(grant, sealed.runtimeId, request) };
|
|
232907
|
+
}
|
|
232908
|
+
const jwt = await dependencies.resolveJwt(sessionId, { forceRefresh: false });
|
|
232909
|
+
if (jwt === null) return missingAuthority();
|
|
232910
|
+
return await requestOwnerTicket(jwt, {
|
|
232911
|
+
...request,
|
|
232912
|
+
runtimeId: sealed.runtimeId
|
|
232913
|
+
});
|
|
232914
|
+
},
|
|
232915
|
+
readCredential
|
|
232916
|
+
});
|
|
232917
|
+
return await prepare(String(input.threadId), acquired);
|
|
232918
|
+
};
|
|
232919
|
+
}
|
|
232920
|
+
//#endregion
|
|
231890
232921
|
//#region src/provider/claudeRuntimeReadinessGuard.ts
|
|
231891
232922
|
const GENERATION_HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
231892
232923
|
const RUNTIME_READINESS_TIMEOUT_MS$1 = 1e4;
|
|
@@ -232155,7 +233186,22 @@ function makeClaudeRuntimeReadinessGuard(config, dependencies) {
|
|
|
232155
233186
|
return (input) => claudeReadinessGuardEffect(config, dependencies, resolved, input);
|
|
232156
233187
|
}
|
|
232157
233188
|
function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
|
|
232158
|
-
if (!requiresNativeClaudeReadiness(config, input.provider)) return succeed(null);
|
|
233189
|
+
if (!requiresNativeClaudeReadiness(config, input.provider, input.providerAccountId)) return succeed(null);
|
|
233190
|
+
if (input.providerAccountId !== void 0) {
|
|
233191
|
+
const prepareSelectedCredential = dependencies.prepareSelectedCredential ?? makeClaudeSelectedCredentialPreparer(config, {
|
|
233192
|
+
resolveJwt: dependencies.resolveJwt,
|
|
233193
|
+
readSessionId: resolved.readSessionId,
|
|
233194
|
+
readRuntimeAuthority: resolved.readRuntimeAuthority
|
|
233195
|
+
});
|
|
233196
|
+
const selectedInput = {
|
|
233197
|
+
...input,
|
|
233198
|
+
providerAccountId: input.providerAccountId
|
|
233199
|
+
};
|
|
233200
|
+
return tryPromise({
|
|
233201
|
+
try: () => prepareSelectedCredential(selectedInput),
|
|
233202
|
+
catch: () => selectedCredentialValidationError(input.operation, selectedInput.providerAccountId)
|
|
233203
|
+
});
|
|
233204
|
+
}
|
|
232159
233205
|
const authority = resolveGuardAuthority(config, resolved.readSessionId, input);
|
|
232160
233206
|
if (authority === null) return invalidOwnerSessionEffect(input.operation);
|
|
232161
233207
|
return tryPromise({
|
|
@@ -232163,8 +233209,9 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
|
|
|
232163
233209
|
catch: (cause) => readinessValidationError(input.operation, cause)
|
|
232164
233210
|
});
|
|
232165
233211
|
}
|
|
232166
|
-
function requiresNativeClaudeReadiness(config, provider) {
|
|
232167
|
-
|
|
233212
|
+
function requiresNativeClaudeReadiness(config, provider, providerAccountId) {
|
|
233213
|
+
if (provider !== "claudeAgent" || config.forgeCmmdRuntimeMode !== true) return false;
|
|
233214
|
+
return providerAccountId !== void 0 || personalClaudeInVmEnabled();
|
|
232168
233215
|
}
|
|
232169
233216
|
function resolveGuardAuthority(config, readSessionId, input) {
|
|
232170
233217
|
const runtimeId = config.forgeCmmdRuntimeId?.trim();
|
|
@@ -232201,6 +233248,12 @@ async function verifyGuardAuthority(config, dependencies, resolved, authority) {
|
|
|
232201
233248
|
fetchReadiness: resolved.fetchReadiness
|
|
232202
233249
|
});
|
|
232203
233250
|
}
|
|
233251
|
+
function selectedCredentialValidationError(operation, accountId) {
|
|
233252
|
+
return new ProviderValidationError({
|
|
233253
|
+
operation,
|
|
233254
|
+
issue: `The selected Claude account ${accountId} could not be prepared. Reconnect the account and try again.`
|
|
233255
|
+
});
|
|
233256
|
+
}
|
|
232204
233257
|
function readinessValidationError(operation, cause) {
|
|
232205
233258
|
return new ProviderValidationError({
|
|
232206
233259
|
operation,
|
|
@@ -267698,7 +268751,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
267698
268751
|
var dgram = __require("dgram");
|
|
267699
268752
|
var thunky = require_thunky();
|
|
267700
268753
|
var events = __require("events");
|
|
267701
|
-
var os = __require("os");
|
|
268754
|
+
var os$1 = __require("os");
|
|
267702
268755
|
var noop = function() {};
|
|
267703
268756
|
module.exports = function(opts) {
|
|
267704
268757
|
if (!opts) opts = {};
|
|
@@ -267829,14 +268882,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
267829
268882
|
return that;
|
|
267830
268883
|
};
|
|
267831
268884
|
function defaultInterface() {
|
|
267832
|
-
var networks = os.networkInterfaces();
|
|
268885
|
+
var networks = os$1.networkInterfaces();
|
|
267833
268886
|
var names = Object.keys(networks);
|
|
267834
268887
|
for (var i = 0; i < names.length; i++) {
|
|
267835
268888
|
var net = networks[names[i]];
|
|
267836
268889
|
for (var j = 0; j < net.length; j++) {
|
|
267837
268890
|
var iface = net[j];
|
|
267838
268891
|
if (isIPv4(iface.family) && !iface.internal) {
|
|
267839
|
-
if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
|
|
268892
|
+
if (os$1.platform() === "darwin" && names[i] === "en0") return iface.address;
|
|
267840
268893
|
return "0.0.0.0";
|
|
267841
268894
|
}
|
|
267842
268895
|
}
|
|
@@ -267844,7 +268897,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
267844
268897
|
return "127.0.0.1";
|
|
267845
268898
|
}
|
|
267846
268899
|
function allInterfaces() {
|
|
267847
|
-
var networks = os.networkInterfaces();
|
|
268900
|
+
var networks = os$1.networkInterfaces();
|
|
267848
268901
|
var names = Object.keys(networks);
|
|
267849
268902
|
var res = [];
|
|
267850
268903
|
for (var i = 0; i < names.length; i++) {
|
|
@@ -290898,6 +291951,10 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
|
|
|
290898
291951
|
shouldRestartForAccountChange,
|
|
290899
291952
|
hasResumeCursor: resumeCursor !== void 0
|
|
290900
291953
|
});
|
|
291954
|
+
if (shouldRestartForAccountChange) yield* providerService.stopSession({
|
|
291955
|
+
threadId,
|
|
291956
|
+
preserveResumeState: true
|
|
291957
|
+
});
|
|
290901
291958
|
const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
|
|
290902
291959
|
yield* logInfo("provider command reactor restarted provider session", {
|
|
290903
291960
|
threadId,
|
|
@@ -296833,7 +297890,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
296833
297890
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
296834
297891
|
*/
|
|
296835
297892
|
function readBakedBuildCommit() {
|
|
296836
|
-
return "
|
|
297893
|
+
return "81ce9360f406bcb5ae9fbdc9f90dd55d33f25f0c";
|
|
296837
297894
|
}
|
|
296838
297895
|
async function resolveServerBuildCommit(input) {
|
|
296839
297896
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -298246,20 +299303,20 @@ const generatePairingToken = () => {
|
|
|
298246
299303
|
const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
|
|
298247
299304
|
return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
|
|
298248
299305
|
};
|
|
299306
|
+
const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
|
|
299307
|
+
message,
|
|
299308
|
+
status: 401
|
|
299309
|
+
});
|
|
299310
|
+
const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
|
|
299311
|
+
message,
|
|
299312
|
+
status: 500,
|
|
299313
|
+
cause
|
|
299314
|
+
});
|
|
298249
299315
|
const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(function* () {
|
|
298250
299316
|
const config = yield* ServerConfig;
|
|
298251
299317
|
const pairingLinks = yield* AuthPairingLinkRepository;
|
|
298252
299318
|
const seededGrantsRef = yield* make$50(/* @__PURE__ */ new Map());
|
|
298253
299319
|
const changesPubSub = yield* unbounded$1();
|
|
298254
|
-
const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
|
|
298255
|
-
message,
|
|
298256
|
-
status: 401
|
|
298257
|
-
});
|
|
298258
|
-
const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
|
|
298259
|
-
message,
|
|
298260
|
-
status: 500,
|
|
298261
|
-
cause
|
|
298262
|
-
});
|
|
298263
299320
|
const seedGrant = (credential, grant) => update$4(seededGrantsRef, (current) => {
|
|
298264
299321
|
const next = new Map(current);
|
|
298265
299322
|
next.set(credential, grant);
|
|
@@ -298274,19 +299331,19 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298274
299331
|
id
|
|
298275
299332
|
}).pipe(asVoid);
|
|
298276
299333
|
if (config.desktopBootstrapToken) {
|
|
298277
|
-
const now$
|
|
299334
|
+
const now$3 = yield* now;
|
|
298278
299335
|
yield* seedGrant(config.desktopBootstrapToken, {
|
|
298279
299336
|
method: "desktop-bootstrap",
|
|
298280
299337
|
role: "owner",
|
|
298281
299338
|
subject: "desktop-bootstrap",
|
|
298282
|
-
expiresAt: add$3(now$
|
|
299339
|
+
expiresAt: add$3(now$3, { milliseconds: toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
|
|
298283
299340
|
remainingUses: "unbounded"
|
|
298284
299341
|
});
|
|
298285
299342
|
}
|
|
298286
299343
|
const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
|
|
298287
299344
|
const listActive = () => gen(function* () {
|
|
298288
|
-
const now$
|
|
298289
|
-
return (yield* pairingLinks.listActive({ now: now$
|
|
299345
|
+
const now$2 = yield* now;
|
|
299346
|
+
return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
|
|
298290
299347
|
id: row.id,
|
|
298291
299348
|
credential: row.credential,
|
|
298292
299349
|
role: row.role,
|
|
@@ -298316,8 +299373,8 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298316
299373
|
const id = crypto.randomUUID();
|
|
298317
299374
|
const credential = generatePairingToken();
|
|
298318
299375
|
const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
|
|
298319
|
-
const now$
|
|
298320
|
-
const expiresAt = add$3(now$
|
|
299376
|
+
const now$5 = yield* now;
|
|
299377
|
+
const expiresAt = add$3(now$5, { milliseconds: toMillis(ttl) });
|
|
298321
299378
|
const issued = {
|
|
298322
299379
|
id,
|
|
298323
299380
|
credential,
|
|
@@ -298331,7 +299388,7 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298331
299388
|
role: input?.role ?? "client",
|
|
298332
299389
|
subject: input?.subject ?? "one-time-token",
|
|
298333
299390
|
label: input?.label ?? null,
|
|
298334
|
-
createdAt: now$
|
|
299391
|
+
createdAt: now$5,
|
|
298335
299392
|
expiresAt
|
|
298336
299393
|
});
|
|
298337
299394
|
yield* emitUpsert({
|
|
@@ -298340,13 +299397,13 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298340
299397
|
role: input?.role ?? "client",
|
|
298341
299398
|
subject: input?.subject ?? "one-time-token",
|
|
298342
299399
|
...input?.label ? { label: input.label } : {},
|
|
298343
|
-
createdAt: now$
|
|
299400
|
+
createdAt: now$5,
|
|
298344
299401
|
expiresAt
|
|
298345
299402
|
});
|
|
298346
299403
|
return issued;
|
|
298347
299404
|
}).pipe(mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
|
|
298348
299405
|
const consume = (credential) => gen(function* () {
|
|
298349
|
-
const now$
|
|
299406
|
+
const now$4 = yield* now;
|
|
298350
299407
|
const seededResult = yield* modify$3(seededGrantsRef, (current) => {
|
|
298351
299408
|
const grant = current.get(credential);
|
|
298352
299409
|
if (!grant) return [{
|
|
@@ -298355,7 +299412,7 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298355
299412
|
error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
|
|
298356
299413
|
}, current];
|
|
298357
299414
|
const next = new Map(current);
|
|
298358
|
-
if (isGreaterThanOrEqualTo$1(now$
|
|
299415
|
+
if (isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
|
|
298359
299416
|
next.delete(credential);
|
|
298360
299417
|
return [{
|
|
298361
299418
|
_tag: "error",
|
|
@@ -298384,8 +299441,8 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298384
299441
|
if (seededResult.reason !== "not-found") return yield* seededResult.error;
|
|
298385
299442
|
const consumed = yield* pairingLinks.consumeAvailable({
|
|
298386
299443
|
credential,
|
|
298387
|
-
consumedAt: now$
|
|
298388
|
-
now: now$
|
|
299444
|
+
consumedAt: now$4,
|
|
299445
|
+
now: now$4
|
|
298389
299446
|
});
|
|
298390
299447
|
if (isSome(consumed)) {
|
|
298391
299448
|
yield* emitRemoved(consumed.value.id);
|
|
@@ -298401,9 +299458,9 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
|
|
|
298401
299458
|
if (isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
298402
299459
|
if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
298403
299460
|
if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
298404
|
-
if (isGreaterThanOrEqualTo$1(now$
|
|
299461
|
+
if (isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
|
|
298405
299462
|
return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
298406
|
-
}).pipe(mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
|
|
299463
|
+
}).pipe(mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)), tapError((error) => error.status === 401 ? logWarning$1("Rejected bootstrap credential exchange.", { reason: error.message }) : void_$1));
|
|
298407
299464
|
return {
|
|
298408
299465
|
issueOneTimeToken,
|
|
298409
299466
|
listActive,
|
|
@@ -298458,6 +299515,10 @@ const SetAuthSessionLastConnectedAtInput = Struct({
|
|
|
298458
299515
|
sessionId: AuthSessionId,
|
|
298459
299516
|
lastConnectedAt: DateTimeUtcFromString
|
|
298460
299517
|
});
|
|
299518
|
+
const ExtendAuthSessionExpiryInput = Struct({
|
|
299519
|
+
sessionId: AuthSessionId,
|
|
299520
|
+
expiresAt: DateTimeUtcFromString
|
|
299521
|
+
});
|
|
298461
299522
|
var AuthSessionRepository = class extends Service$1()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
|
|
298462
299523
|
//#endregion
|
|
298463
299524
|
//#region src/persistence/Layers/AuthSessions.ts
|
|
@@ -298609,6 +299670,17 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
|
|
|
298609
299670
|
WHERE session_id = ${sessionId}
|
|
298610
299671
|
AND revoked_at IS NULL
|
|
298611
299672
|
RETURNING session_id AS "sessionId"
|
|
299673
|
+
`
|
|
299674
|
+
});
|
|
299675
|
+
const extendSessionExpiryRows = findAll({
|
|
299676
|
+
Request: ExtendAuthSessionExpiryInput,
|
|
299677
|
+
Result: Struct({ sessionId: AuthSessionId }),
|
|
299678
|
+
execute: ({ sessionId, expiresAt }) => sql`
|
|
299679
|
+
UPDATE auth_sessions
|
|
299680
|
+
SET expires_at = ${expiresAt}
|
|
299681
|
+
WHERE session_id = ${sessionId}
|
|
299682
|
+
AND revoked_at IS NULL
|
|
299683
|
+
RETURNING session_id AS "sessionId"
|
|
298612
299684
|
`
|
|
298613
299685
|
});
|
|
298614
299686
|
const revokeOtherSessionRows = findAll({
|
|
@@ -298631,13 +299703,15 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
|
|
|
298631
299703
|
const revoke = (input) => revokeSessionRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows")), map$3((rows) => rows.length > 0));
|
|
298632
299704
|
const revokeAllExcept = (input) => revokeOtherSessionRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows")), map$3((rows) => rows.map((row) => row.sessionId)));
|
|
298633
299705
|
const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
|
|
299706
|
+
const extendExpiry = (input) => extendSessionExpiryRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.extendExpiry:query", "AuthSessionRepository.extendExpiry:decodeRows")), map$3((rows) => rows.length > 0));
|
|
298634
299707
|
return {
|
|
298635
299708
|
create,
|
|
298636
299709
|
getById,
|
|
298637
299710
|
listActive,
|
|
298638
299711
|
revoke,
|
|
298639
299712
|
revokeAllExcept,
|
|
298640
|
-
setLastConnectedAt
|
|
299713
|
+
setLastConnectedAt,
|
|
299714
|
+
extendExpiry
|
|
298641
299715
|
};
|
|
298642
299716
|
}));
|
|
298643
299717
|
//#endregion
|
|
@@ -298645,6 +299719,20 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
|
|
|
298645
299719
|
const SIGNING_SECRET_NAME = "server-signing-key";
|
|
298646
299720
|
const DEFAULT_SESSION_TTL = days(30);
|
|
298647
299721
|
const DEFAULT_WEBSOCKET_TOKEN_TTL = minutes(5);
|
|
299722
|
+
/**
|
|
299723
|
+
* Whether a session presented at `nowMs` has used up half its own lifetime.
|
|
299724
|
+
*
|
|
299725
|
+
* A phone that opens the app daily should never hit the 30-day wall, but a
|
|
299726
|
+
* lost phone's session should still die on schedule if it is never presented
|
|
299727
|
+
* again. Sliding the expiry forward only when the session is already more
|
|
299728
|
+
* than half spent gives an active phone many chances to renew before expiry
|
|
299729
|
+
* while keeping a dormant session's remaining lifetime bounded by the last
|
|
299730
|
+
* time it was actually used.
|
|
299731
|
+
*/
|
|
299732
|
+
function isPastHalfLife(input) {
|
|
299733
|
+
const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
|
|
299734
|
+
return input.nowMs >= halfLifeMarkMs;
|
|
299735
|
+
}
|
|
298648
299736
|
const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
|
|
298649
299737
|
message,
|
|
298650
299738
|
cause
|
|
@@ -298805,6 +299893,42 @@ const SessionCredentialServiceLive = effect(SessionCredentialService, gen(functi
|
|
|
298805
299893
|
role: claims.role
|
|
298806
299894
|
};
|
|
298807
299895
|
}).pipe(mapError(toSessionCredentialError("Failed to issue session credential.")));
|
|
299896
|
+
/**
|
|
299897
|
+
* Slide a session's expiry forward when the token it just verified is past
|
|
299898
|
+
* half its own lifetime, so a phone that keeps using the app never runs
|
|
299899
|
+
* into the 30-day wall.
|
|
299900
|
+
*
|
|
299901
|
+
* The session id, subject, role and method carry over unchanged — only the
|
|
299902
|
+
* signature-bearing `iat`/`exp` differ — which is what lets the DB row be
|
|
299903
|
+
* extended in place (`extendExpiry`) rather than replaced: everything keyed
|
|
299904
|
+
* by session id, most importantly the `command_access_tokens` row holding
|
|
299905
|
+
* this session's CMMD credential, needs no migration.
|
|
299906
|
+
*/
|
|
299907
|
+
const renewIfPastHalfLife = (input) => gen(function* () {
|
|
299908
|
+
const nowMs = yield* currentTimeMillis;
|
|
299909
|
+
if (!isPastHalfLife({
|
|
299910
|
+
issuedAtMs: input.row.issuedAt.epochMilliseconds,
|
|
299911
|
+
expiresAtMs: input.row.expiresAt.epochMilliseconds,
|
|
299912
|
+
nowMs
|
|
299913
|
+
})) return void 0;
|
|
299914
|
+
const newIssuedAt = yield* now;
|
|
299915
|
+
const newExpiresAt = add$3(newIssuedAt, { milliseconds: toMillis(DEFAULT_SESSION_TTL) });
|
|
299916
|
+
const newClaims = {
|
|
299917
|
+
...input.claims,
|
|
299918
|
+
iat: newIssuedAt.epochMilliseconds,
|
|
299919
|
+
exp: newExpiresAt.epochMilliseconds
|
|
299920
|
+
};
|
|
299921
|
+
const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
|
|
299922
|
+
const newSignature = signPayload(newEncodedPayload, signingSecret);
|
|
299923
|
+
if (!(yield* authSessions.extendExpiry({
|
|
299924
|
+
sessionId: input.claims.sid,
|
|
299925
|
+
expiresAt: newExpiresAt
|
|
299926
|
+
}).pipe(orElseSucceed(() => false)))) return void 0;
|
|
299927
|
+
return {
|
|
299928
|
+
token: `${newEncodedPayload}.${newSignature}`,
|
|
299929
|
+
expiresAt: newExpiresAt
|
|
299930
|
+
};
|
|
299931
|
+
});
|
|
298808
299932
|
const verify = (token) => gen(function* () {
|
|
298809
299933
|
const [encodedPayload, signature] = token.split(".");
|
|
298810
299934
|
if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
|
|
@@ -298819,14 +299943,19 @@ const SessionCredentialServiceLive = effect(SessionCredentialService, gen(functi
|
|
|
298819
299943
|
if (isSome(row)) {
|
|
298820
299944
|
if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
|
|
298821
299945
|
} else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
|
|
299946
|
+
const renewed = isSome(row) ? yield* renewIfPastHalfLife({
|
|
299947
|
+
claims,
|
|
299948
|
+
row: row.value
|
|
299949
|
+
}) : void 0;
|
|
298822
299950
|
return {
|
|
298823
299951
|
sessionId: claims.sid,
|
|
298824
299952
|
token,
|
|
298825
299953
|
method: claims.method,
|
|
298826
299954
|
client: isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
|
|
298827
|
-
expiresAt: makeUnsafe$5(claims.exp),
|
|
299955
|
+
expiresAt: renewed?.expiresAt ?? makeUnsafe$5(claims.exp),
|
|
298828
299956
|
subject: claims.sub,
|
|
298829
|
-
role: claims.role
|
|
299957
|
+
role: claims.role,
|
|
299958
|
+
...renewed ? { renewed } : {}
|
|
298830
299959
|
};
|
|
298831
299960
|
}).pipe(mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
|
|
298832
299961
|
message: "Failed to verify session credential.",
|
|
@@ -299318,7 +300447,10 @@ const ServerAuthLive = effect(ServerAuth, gen(function* () {
|
|
|
299318
300447
|
subject: session.subject,
|
|
299319
300448
|
method: session.method,
|
|
299320
300449
|
role: session.role,
|
|
299321
|
-
...session.
|
|
300450
|
+
...session.renewed ? {
|
|
300451
|
+
expiresAt: session.renewed.expiresAt,
|
|
300452
|
+
renewedSessionToken: session.renewed.token
|
|
300453
|
+
} : session.expiresAt ? { expiresAt: session.expiresAt } : {}
|
|
299322
300454
|
})), mapError((cause) => new AuthError({
|
|
299323
300455
|
message: "Unauthorized request.",
|
|
299324
300456
|
status: 401,
|
|
@@ -299353,6 +300485,7 @@ const ServerAuthLive = effect(ServerAuth, gen(function* () {
|
|
|
299353
300485
|
role: session.role,
|
|
299354
300486
|
sessionMethod: session.method,
|
|
299355
300487
|
...session.expiresAt ? { expiresAt: toUtc(session.expiresAt) } : {},
|
|
300488
|
+
...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
|
|
299356
300489
|
cmmdIdentity
|
|
299357
300490
|
})))), tapError((e) => {
|
|
299358
300491
|
if (e._tag === "AuthError" && (e.status ?? 500) < 500) return void_$1;
|
|
@@ -305679,6 +306812,82 @@ const orchestrationAutomationTerminalReconcileRouteLayer = add$1("POST", "/api/o
|
|
|
305679
306812
|
}, { status: 200 });
|
|
305680
306813
|
}).pipe(catchTag("OrchestrationDispatchCommandError", respondToWorkRunEventError)));
|
|
305681
306814
|
//#endregion
|
|
306815
|
+
//#region src/orchestration/threadSnapshotRoute.ts
|
|
306816
|
+
/**
|
|
306817
|
+
* `GET /api/orchestration/threads/:threadId`, one thread's detail snapshot,
|
|
306818
|
+
* optionally windowed to a page of recent turns.
|
|
306819
|
+
*
|
|
306820
|
+
* Forge never served this route. The mobile client has asked for it on every
|
|
306821
|
+
* thread open since it shipped, fell through to the socket-embedded snapshot on
|
|
306822
|
+
* the 404, and its "load earlier" control had nowhere to fetch from, so a long
|
|
306823
|
+
* thread was only ever loadable in full. Serving it here gives the initial read
|
|
306824
|
+
* a body the transport can gzip, off the RPC socket, and gives older turns a
|
|
306825
|
+
* cursor to arrive on.
|
|
306826
|
+
*
|
|
306827
|
+
* The window is applied by the same pure function the WebSocket fallback
|
|
306828
|
+
* snapshot uses, so the two reads cannot disagree about what a page is.
|
|
306829
|
+
*/
|
|
306830
|
+
/**
|
|
306831
|
+
* Reads the window out of the query string.
|
|
306832
|
+
*
|
|
306833
|
+
* Returns null for a malformed value rather than ignoring it. A client that
|
|
306834
|
+
* asked for 10 turns and silently received 4,000 has no way to tell, and the
|
|
306835
|
+
* failure would show up as a slow app rather than as an error.
|
|
306836
|
+
*/
|
|
306837
|
+
function parseThreadSnapshotWindow(searchParams) {
|
|
306838
|
+
const rawTurnLimit = searchParams.get("turnLimit");
|
|
306839
|
+
const rawBeforeCursor = searchParams.get("beforeCursor");
|
|
306840
|
+
let turnLimit;
|
|
306841
|
+
if (rawTurnLimit !== null) {
|
|
306842
|
+
const parsed = Number(rawTurnLimit);
|
|
306843
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) return null;
|
|
306844
|
+
turnLimit = parsed;
|
|
306845
|
+
}
|
|
306846
|
+
let beforeCursor;
|
|
306847
|
+
if (rawBeforeCursor !== null) {
|
|
306848
|
+
const trimmed = rawBeforeCursor.trim();
|
|
306849
|
+
if (trimmed.length === 0) return null;
|
|
306850
|
+
beforeCursor = trimmed;
|
|
306851
|
+
}
|
|
306852
|
+
return {
|
|
306853
|
+
...turnLimit === void 0 ? {} : { turnLimit },
|
|
306854
|
+
...beforeCursor === void 0 ? {} : { beforeCursor }
|
|
306855
|
+
};
|
|
306856
|
+
}
|
|
306857
|
+
const orchestrationThreadSnapshotRouteLayer = add$1("GET", "/api/orchestration/threads/:threadId", gen(function* () {
|
|
306858
|
+
const request = yield* HttpServerRequest;
|
|
306859
|
+
yield* (yield* ServerAuth).authenticateHttpRequest(request);
|
|
306860
|
+
const rawThreadId = (yield* params)["threadId"];
|
|
306861
|
+
if (rawThreadId === void 0 || rawThreadId.length === 0) return jsonUnsafe({ error: "Missing thread id." }, { status: 400 });
|
|
306862
|
+
const url = toURL(request);
|
|
306863
|
+
const window = isNone(url) ? {} : parseThreadSnapshotWindow(url.value.searchParams);
|
|
306864
|
+
if (window === null) return jsonUnsafe({ error: "Invalid turnLimit or beforeCursor." }, { status: 400 });
|
|
306865
|
+
const threadId = ThreadId.make(decodeURIComponent(rawThreadId));
|
|
306866
|
+
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
|
|
306867
|
+
const orchestrationEngine = yield* OrchestrationEngineService;
|
|
306868
|
+
const [threadDetail, snapshotSequence] = yield* all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
|
|
306869
|
+
message: `Failed to load thread ${threadId}`,
|
|
306870
|
+
cause
|
|
306871
|
+
}))), orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
|
|
306872
|
+
if (isNone(threadDetail)) return jsonUnsafe({ error: "Thread not found." }, { status: 404 });
|
|
306873
|
+
const windowed = windowThreadDetail({
|
|
306874
|
+
thread: threadDetail.value,
|
|
306875
|
+
window,
|
|
306876
|
+
snapshotSequence
|
|
306877
|
+
});
|
|
306878
|
+
return jsonUnsafe({
|
|
306879
|
+
snapshotSequence,
|
|
306880
|
+
thread: windowed.thread,
|
|
306881
|
+
...windowed.page === void 0 ? {} : { page: windowed.page }
|
|
306882
|
+
}, { status: 200 });
|
|
306883
|
+
}).pipe(catchTag("AuthError", respondToAuthError), catchTag("OrchestrationGetSnapshotError", (error) => gen(function* () {
|
|
306884
|
+
yield* logError("thread snapshot route failed", {
|
|
306885
|
+
message: error.message,
|
|
306886
|
+
cause: error.cause
|
|
306887
|
+
});
|
|
306888
|
+
return jsonUnsafe({ error: error.message }, { status: 500 });
|
|
306889
|
+
}))));
|
|
306890
|
+
//#endregion
|
|
305682
306891
|
//#region src/localAutomations/scheduling.ts
|
|
305683
306892
|
const MINUTE_MS = 6e4;
|
|
305684
306893
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
@@ -317547,7 +318756,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
317547
318756
|
const sql = yield* SqlClient;
|
|
317548
318757
|
const fetchFn = options.fetchFn ?? fetch;
|
|
317549
318758
|
const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
|
|
317550
|
-
const resolveStoredJwt = (forceRefresh) => gen(function* () {
|
|
318759
|
+
const resolveStoredJwt = (forceRefresh, staleJwt) => gen(function* () {
|
|
317551
318760
|
const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(provideService(SqlClient, sql), mapError((cause) => new ProjectLeadControllerClientError({
|
|
317552
318761
|
kind: "unavailable",
|
|
317553
318762
|
operation: "resolve-owner-session",
|
|
@@ -317561,7 +318770,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
317561
318770
|
sessionId: owner.sessionId,
|
|
317562
318771
|
subject: owner.subject,
|
|
317563
318772
|
logPrefix: "project-lead-scheduler",
|
|
317564
|
-
forceRefresh
|
|
318773
|
+
forceRefresh,
|
|
318774
|
+
staleAccessToken: staleJwt
|
|
317565
318775
|
}).pipe(provideService(SqlClient, sql));
|
|
317566
318776
|
if (!jwt) return yield* new ProjectLeadControllerClientError({
|
|
317567
318777
|
kind: "unauthenticated",
|
|
@@ -317604,7 +318814,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
317604
318814
|
}));
|
|
317605
318815
|
if (first._tag === "Success") return first.success;
|
|
317606
318816
|
if (first.failure.kind !== "unauthenticated") return yield* first.failure;
|
|
317607
|
-
const refreshed = yield* resolveJwt(true);
|
|
318817
|
+
const refreshed = yield* resolveJwt(true, jwt);
|
|
317608
318818
|
return yield* requestAttempt({
|
|
317609
318819
|
...input,
|
|
317610
318820
|
jwt: refreshed
|
|
@@ -319998,12 +321208,12 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", gen(func
|
|
|
319998
321208
|
//#region src/server.ts
|
|
319999
321209
|
const PtyAdapterLive = unwrap(gen(function* () {
|
|
320000
321210
|
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunPTY-DkAIzLIx.mjs"))).layer;
|
|
320001
|
-
else return (yield* promise(() => import("./NodePTY-
|
|
321211
|
+
else return (yield* promise(() => import("./NodePTY-BhezYy9H.mjs"))).layer;
|
|
320002
321212
|
}));
|
|
320003
321213
|
const HttpServerLive = unwrap(gen(function* () {
|
|
320004
321214
|
const config = yield* ServerConfig;
|
|
320005
|
-
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-
|
|
320006
|
-
const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-
|
|
321215
|
+
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-FbbQmsl_.mjs"))).layer(bunHttpServerOptions(config));
|
|
321216
|
+
const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-BJ4l15f-.mjs")), promise(() => import("node:http"))]);
|
|
320007
321217
|
return NodeHttpServer.layer(NodeHttp.createServer, {
|
|
320008
321218
|
host: config.host,
|
|
320009
321219
|
port: config.port
|
|
@@ -320011,10 +321221,10 @@ const HttpServerLive = unwrap(gen(function* () {
|
|
|
320011
321221
|
}));
|
|
320012
321222
|
const PlatformServicesLive = unwrap(gen(function* () {
|
|
320013
321223
|
if (typeof Bun !== "undefined") {
|
|
320014
|
-
const { layer } = yield* promise(() => import("./BunServices-
|
|
321224
|
+
const { layer } = yield* promise(() => import("./BunServices-DC7nhA3w.mjs"));
|
|
320015
321225
|
return layer;
|
|
320016
321226
|
} else {
|
|
320017
|
-
const { layer } = yield* promise(() => import("./NodeServices-
|
|
321227
|
+
const { layer } = yield* promise(() => import("./NodeServices-D3IxOzHl.mjs").then((n) => n.t));
|
|
320018
321228
|
return layer;
|
|
320019
321229
|
}
|
|
320020
321230
|
}));
|
|
@@ -320079,7 +321289,7 @@ const MiscRuntimeLayerLive = mergeAll(AnalyticsServiceLayerLive, OpenLive, Serve
|
|
|
320079
321289
|
const RuntimeDependenciesWithoutPullRequestsLive = ReactorLayerLive.pipe(provideMerge(CheckpointingLayerLive), provideMerge(GitLayerLive), provideMerge(OrchestrationLayerLive), provideMerge(ProviderLayerLive), provideMerge(TerminalLayerLive), provideMerge(AgentBrowserManagerLive), provideMerge(mergeAll(ThreadDevRuntimeLive, DevTunnelLive.pipe(provide$1(ThreadDevRuntimeLive)))), provideMerge(PersistenceLayerLive), provideMerge(KeybindingsLive), provideMerge(ProviderRegistryLive), provideMerge(mergeAll(ServerSettingsLive, UsageServiceLive.pipe(provide$1(ServerSettingsLive), provide$1(UsageSessionLookupLive), provide$1(UsageRecordedSourceLive.pipe(provide$1(OrchestrationLayerLive), provide$1(RuntimeReplicationOutboxLive), provide$1(ProviderSessionRuntimeRepositoryLive), provide$1(TextGenerationSessionRepositoryLive)))))), provideMerge(WorkspaceLayerLive), provideMerge(WorkspaceReadinessStateLive), provideMerge(ProjectFaviconResolverLive), provideMerge(RepositoryIdentityResolverLive), provideMerge(ServerEnvironmentLive), provideMerge(AuthLayerLive), provideMerge(ServerSecretStoreLayerLive), provideMerge(mergeAll(CloudRedisCoordinationLive, CloudStateSyncLive.pipe(provide$1(PersistenceLayerLive)))), provideMerge(MiscRuntimeLayerLive));
|
|
320080
321290
|
const RuntimeDependenciesLive = pullRequestServiceFactoryLayer.pipe(provideMerge(RuntimeDependenciesWithoutPullRequestsLive), provideMerge(cmmdProjectIdentityResolverLive.pipe(provide$1(RuntimeDependenciesWithoutPullRequestsLive))), provideMerge(pullRequestProviderRuntimeLive));
|
|
320081
321291
|
const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(provideMerge(RuntimeDependenciesLive));
|
|
320082
|
-
const makeRoutesLayer = mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(provide$1(browserApiCorsLayer));
|
|
321292
|
+
const makeRoutesLayer = mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRenewRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, orchestrationThreadSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(provide$1(browserApiCorsLayer));
|
|
320083
321293
|
const runServer = launch$1(unwrap(gen(function* () {
|
|
320084
321294
|
const config = yield* ServerConfig;
|
|
320085
321295
|
fixPath({ preferProcessPath: config.forgeCmmdRuntimeMode === true });
|
|
@@ -321009,7 +322219,7 @@ const connectLoginCommand = make$34("login", {
|
|
|
321009
322219
|
})).json();
|
|
321010
322220
|
},
|
|
321011
322221
|
openBrowser: async (url) => {
|
|
321012
|
-
const open = (await import("./open-
|
|
322222
|
+
const open = (await import("./open-FIgTDosf.mjs")).default;
|
|
321013
322223
|
await open(url);
|
|
321014
322224
|
return true;
|
|
321015
322225
|
},
|