@botbuddy/cli 1.63.13 → 1.63.14
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/stack-identified-admission.mjs +17 -0
- package/src/stack-isolation.mjs +57 -3
- package/src/stack-requester-origin-config.mjs +21 -0
- package/src/stack-requester-origin.mjs +62 -0
- package/src/stack.mjs +263 -10
- package/src/vendor/smol-toml/LICENSE +24 -0
- package/src/vendor/smol-toml/date.js +156 -0
- package/src/vendor/smol-toml/error.js +67 -0
- package/src/vendor/smol-toml/extract.js +293 -0
- package/src/vendor/smol-toml/index.js +34 -0
- package/src/vendor/smol-toml/parse.js +164 -0
- package/src/vendor/smol-toml/primitive.js +151 -0
- package/src/vendor/smol-toml/stringify.js +219 -0
- package/src/vendor/smol-toml/struct.js +156 -0
- package/src/vendor/smol-toml/util.js +57 -0
- package/src/vendor/smol-toml/vendored.json +20 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.14",
|
|
3
|
+
"source_commit": "8ec06459dcd6e698ce14b725eb4b6691083ec445",
|
|
4
|
+
"source_sha256": "b72415fa0cd4b8b19a2613c78573c958809c453191717b9e53fabb23afa6f7ea",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "eb7aba5ff1ca0c551f02b1e0ce7e3d136e000443577d4494160da94e3a8ecb68"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.14",
|
|
3
|
+
"source_commit": "8ec06459dcd6e698ce14b725eb4b6691083ec445",
|
|
4
|
+
"source_sha256": "b72415fa0cd4b8b19a2613c78573c958809c453191717b9e53fabb23afa6f7ea",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "fe9452d4139a7db72844293109e0494605853e8cd3f9603043207b6cd892e990"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.63.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.63.14",
|
|
4
|
+
"source_commit": "8ec06459dcd6e698ce14b725eb4b6691083ec445",
|
|
5
|
+
"source_sha256": "b72415fa0cd4b8b19a2613c78573c958809c453191717b9e53fabb23afa6f7ea",
|
|
6
6
|
"targets": {
|
|
7
7
|
"darwin-arm64": {
|
|
8
8
|
"goos": "darwin",
|
|
9
9
|
"goarch": "arm64",
|
|
10
10
|
"binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "eb7aba5ff1ca0c551f02b1e0ce7e3d136e000443577d4494160da94e3a8ecb68",
|
|
12
12
|
"mode": "0755"
|
|
13
13
|
},
|
|
14
14
|
"linux-x64": {
|
|
15
15
|
"goos": "linux",
|
|
16
16
|
"goarch": "amd64",
|
|
17
17
|
"binary": "daemon/bin/linux-x64/botbuddy-daemon",
|
|
18
|
-
"sha256": "
|
|
18
|
+
"sha256": "fe9452d4139a7db72844293109e0494605853e8cd3f9603043207b6cd892e990",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// BOT-2780 consumer of BOT-2499's identified admission ACK (not a second allocator).
|
|
2
|
+
// A durable ACK is recovery identity; pending delivery is never dispatch authority.
|
|
3
|
+
const uuid = value => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
4
|
+
export function identifiedAdmissionAck(value, requestId) {
|
|
5
|
+
if(value?.success!==true||!['ADMITTED','EXACT_REPLAY'].includes(value.code)||!uuid(requestId)||!uuid(value.request_id)||value.request_id.toLowerCase()!==requestId.toLowerCase()||!uuid(value.lease_id)||!['created','readopted'].includes(value.admission_kind)||!['queued','provisioning','active'].includes(value.state)||!['pending','bound','none'].includes(value.job_binding)||!(value.provision_job_id===null||uuid(value.provision_job_id))||((value.job_binding==='bound')!==uuid(value.provision_job_id))||(value.admission_kind==='created'&&value.job_binding==='none')||(value.admission_kind==='readopted'&&value.job_binding!=='none')||!['ready','pending'].includes(value.delivery_status))return null;
|
|
6
|
+
return Object.freeze(Object.fromEntries(['code','request_id','lease_id','admission_kind','state','job_binding','provision_job_id','delivery_status'].map(key=>[key,value[key]])));
|
|
7
|
+
}
|
|
8
|
+
export function identifiedAdmissionDelivery(value, ack) {
|
|
9
|
+
const delivery=value?.delivery;
|
|
10
|
+
if(!ack||ack.delivery_status!=='ready'||!delivery||typeof delivery!=='object'||Array.isArray(delivery)||delivery.success===false||delivery.lease_id!=null&&delivery.lease_id!==ack.lease_id||delivery.state!=null&&delivery.state!==ack.state)return null;
|
|
11
|
+
return {...delivery,success:true,lease_id:ack.lease_id,state:ack.state};
|
|
12
|
+
}
|
|
13
|
+
export function identifiedAdmissionRecovery(request, contextFile, ack=null) {
|
|
14
|
+
return {request_id:request.request_id,context_file:contextFile,
|
|
15
|
+
read_tool:'get_stack_lease_admission',read_args:{request_id:request.request_id},
|
|
16
|
+
...(ack?{committed_ack:ack}:{})};
|
|
17
|
+
}
|
package/src/stack-isolation.mjs
CHANGED
|
@@ -786,6 +786,39 @@ export function stableRunStackReusable(worktreeRoot, dirName, { read = readFileS
|
|
|
786
786
|
return marker.idle_retained === true || marker.released === true;
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
+
/**
|
|
790
|
+
* BOT-2780: a beta requester-origin run never resumes a retained slot context. Its
|
|
791
|
+
* generated links name the listener of the invocation that started that stack, so
|
|
792
|
+
* refuse any unreleased marker for this slot before deriving a new bundle or ports.
|
|
793
|
+
*/
|
|
794
|
+
export function assertFreshRunStackAvailable(worktreeRoot, slot, { read = readFileSync, readdir = readdirSync, lstat = lstatSync } = {}) {
|
|
795
|
+
const refuse = () => {
|
|
796
|
+
const error = new Error("requester origin cannot resume a retained slot context; inspect its owning lease with `bb stack status <lease-id>` and use `bb stack done <lease-id> --reap` only after that owner finishes, then confirm reaped before a fresh run");
|
|
797
|
+
error.code = "REQUESTER_ORIGIN_RETAINED_CONTEXT";
|
|
798
|
+
throw error;
|
|
799
|
+
};
|
|
800
|
+
const stacks = join(worktreeRoot, LEASED_STACKS_DIR);
|
|
801
|
+
let entries;
|
|
802
|
+
try { entries = readdir(stacks); } catch (error) { if (error.code === "ENOENT") return; refuse(); }
|
|
803
|
+
if (entries.length > 4096) refuse();
|
|
804
|
+
for (const entry of entries) {
|
|
805
|
+
const path = join(stacks, entry, LEASED_STACK_MARKER);
|
|
806
|
+
let info;
|
|
807
|
+
try { info = lstat(path); } catch (error) { if (error.code === "ENOENT" || error.code === "ENOTDIR") continue; refuse(); }
|
|
808
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 65536) refuse();
|
|
809
|
+
let marker;
|
|
810
|
+
try { marker = JSON.parse(read(path, "utf8")); } catch { refuse(); }
|
|
811
|
+
if (!marker || typeof marker.slot !== "string") refuse();
|
|
812
|
+
if (marker.slot !== slot || marker.released === true) continue;
|
|
813
|
+
// An expired context whose owner is gone is what materialization GC collects anyway; the
|
|
814
|
+
// control plane still refuses a new remote lease while any lease on the slot is unreaped.
|
|
815
|
+
const ownerAlive = Number.isInteger(marker.owner_pid) && marker.owner_pid > 0 &&
|
|
816
|
+
(!marker.host || marker.host === hostname()) && pidIsAlive(marker.owner_pid);
|
|
817
|
+
const expired = marker.idle_retained !== true && Number.isFinite(marker.retain_until) && Date.now() >= marker.retain_until;
|
|
818
|
+
if (!expired || ownerAlive) refuse();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
789
822
|
/** BOT-1803: record a PROVEN reap on the stack's marker, so the next run may reuse the directory. */
|
|
790
823
|
export function markLeasedStackReleased(stackDir, { read = readFileSync, write = writeFileSync, rename = renameSync } = {}) {
|
|
791
824
|
const marker = readStackMarker(stackDir, read);
|
|
@@ -1246,6 +1279,22 @@ export function bindLeasedStackClaim(stackDir, leaseId, { registryDir = defaultP
|
|
|
1246
1279
|
});
|
|
1247
1280
|
}
|
|
1248
1281
|
|
|
1282
|
+
/**
|
|
1283
|
+
* BOT-2780: associate this invocation's claims and marker with its identified admission
|
|
1284
|
+
* request BEFORE the request leaves, so a possibly committed but never acknowledged lease
|
|
1285
|
+
* can later release them through matching accepted cleanup (recoverLeasedStackClaim).
|
|
1286
|
+
*/
|
|
1287
|
+
export function recordAdmissionRequest(stackDir, requestId, { registryDir = defaultPortRegistryDir() } = {}) {
|
|
1288
|
+
const marker = readStackMarker(stackDir, readFileSync);
|
|
1289
|
+
const token = marker?.invocation_token;
|
|
1290
|
+
if (!token || typeof requestId !== "string" || !requestId) throw new Error("local port claim has no invocation identity");
|
|
1291
|
+
return mutateBoundClaims(stackDir, registryDir, (claims) => {
|
|
1292
|
+
if (claims.some(({ claim }) => claim.invocation_token !== token || claim.lease_id != null)) throw new Error("local port claim was replaced or already bound");
|
|
1293
|
+
for (const { path, claim } of claims) atomicWrite(path, JSON.stringify({ ...claim, admission_request_id: requestId }), { write: writeFileSync, rename: renameSync });
|
|
1294
|
+
atomicWrite(join(stackDir, LEASED_STACK_MARKER), JSON.stringify({ ...marker, admission_request_id: requestId }), { write: writeFileSync, rename: renameSync });
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1249
1298
|
/** Allocation and recovery serialize the inspect/delete against replacement claims. */
|
|
1250
1299
|
function mutateBoundClaims(stackDir, registryDir, mutate) {
|
|
1251
1300
|
const config = readFileSync(join(stackDir, "supabase/config.toml"), "utf8");
|
|
@@ -1286,8 +1335,12 @@ export function recoverLeasedStackClaim(stackDir, leaseId, proof, { registryDir
|
|
|
1286
1335
|
if (!claims.length) return { released: true, reason: "no local claim remains for this stack" };
|
|
1287
1336
|
const token = claims[0].claim.invocation_token;
|
|
1288
1337
|
const project = claims[0].claim.project_id;
|
|
1338
|
+
// BOT-2780: an unbound claim recorded for an identified admission request is this lease's
|
|
1339
|
+
// only when the caller proved (owner-scoped admission read) that request named the lease.
|
|
1340
|
+
const requestBound = (entry) => entry?.lease_id == null && typeof proof.admission_request_id === "string" &&
|
|
1341
|
+
proof.admission_request_id.length > 0 && entry?.admission_request_id === proof.admission_request_id;
|
|
1289
1342
|
if (typeof token !== "string" || !token || typeof project !== "string" || !/^[A-Za-z0-9_-]+$/.test(project) || claims.some(({ claim }) =>
|
|
1290
|
-
claim.lease_id !== leaseId || claim.invocation_token !== token || claim.project_id !== project || claim.host !== hostname() ||
|
|
1343
|
+
(claim.lease_id !== leaseId && !requestBound(claim)) || claim.invocation_token !== token || claim.project_id !== project || claim.host !== hostname() ||
|
|
1291
1344
|
!Number.isInteger(claim.owner_pid) || claim.owner_pid <= 0 || pidIsAlive(claim.owner_pid))) {
|
|
1292
1345
|
return { released: false, reason: "local claim is live, replaced, or lacks this lease binding" };
|
|
1293
1346
|
}
|
|
@@ -1302,7 +1355,7 @@ export function recoverLeasedStackClaim(stackDir, leaseId, proof, { registryDir
|
|
|
1302
1355
|
if (error?.code !== "ENOENT") return { released: false, reason: "stack marker is unreadable or corrupt; local claim retained" };
|
|
1303
1356
|
marker = null;
|
|
1304
1357
|
}
|
|
1305
|
-
if (marker && (marker.invocation_token !== token || marker.lease_id !== leaseId || marker.project_id !== project)) {
|
|
1358
|
+
if (marker && (marker.invocation_token !== token || (marker.lease_id !== leaseId && !requestBound(marker)) || marker.project_id !== project)) {
|
|
1306
1359
|
return { released: false, reason: "stack marker was replaced" };
|
|
1307
1360
|
}
|
|
1308
1361
|
for (const { path } of claims) rmSync(path);
|
|
@@ -1687,7 +1740,8 @@ export function materializeRunStackDir(cwd, { slot, ticket, nonce, idleTtlSecs }
|
|
|
1687
1740
|
const configPath = join(sourceDir, "config.toml");
|
|
1688
1741
|
let rootConfig;
|
|
1689
1742
|
try {
|
|
1690
|
-
|
|
1743
|
+
// BOT-2780: a beta requester-origin run supplies the already patched private copy.
|
|
1744
|
+
rootConfig = io.configText ?? read(configPath, "utf8");
|
|
1691
1745
|
} catch (error) {
|
|
1692
1746
|
throw new Error(
|
|
1693
1747
|
`cannot materialize an isolated leased stack: ${configPath} is missing or unreadable (${error?.code ?? error?.message ?? error}). ` +
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// BOT-2780: parse and roundtrip the private derived config before bundling.
|
|
2
|
+
// The vendored, hash-pinned smol-toml 1.9.1 parser (src/vendor/smol-toml) is loaded only
|
|
3
|
+
// for the beta requester-origin feature; help and legacy paths never import it.
|
|
4
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
5
|
+
const fail=(code,message)=>{const error=Error(message);error.code=code;throw error};
|
|
6
|
+
let parser;
|
|
7
|
+
export async function patchRequesterAuthOrigin(configText,origin){
|
|
8
|
+
let url;try{url=new URL(origin)}catch{fail('REQUESTER_ORIGIN_URL_REFUSED','requester origin refused')}
|
|
9
|
+
if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password||url.pathname!=='/'||url.search||url.hash||origin!==url.origin||!Number.isInteger(Number(url.port))||Number(url.port)<1024||Number(url.port)>65535)fail('REQUESTER_ORIGIN_URL_REFUSED','requester origin must be its reserved loopback HTTP listener');
|
|
10
|
+
if(typeof configText!=='string'||Buffer.byteLength(configText)>1024*1024)fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config is unavailable or exceeds its bound');
|
|
11
|
+
try{parser??=import('./vendor/smol-toml/index.js');const {parse,stringify}=await parser;let document;try{document=parse(configText,{integersAsBigInt:true,unsafeKeyBehaviour:'throw'})}catch{fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config is not supported valid TOML')}
|
|
12
|
+
if(!Object.hasOwn(document,'auth'))Object.defineProperty(document,'auth',{value:{},writable:true,enumerable:true,configurable:true});
|
|
13
|
+
const auth=document.auth;if(!auth||typeof auth!=='object'||Array.isArray(auth))fail('REQUESTER_ORIGIN_AUTH_REFUSED','private auth configuration is ambiguous');
|
|
14
|
+
const target=origin+'/auth/v1';
|
|
15
|
+
if(Object.hasOwn(auth,'external_url')&&(typeof auth.external_url!=='string'||auth.external_url!==''&&auth.external_url!==target))fail('REQUESTER_ORIGIN_EXPLICIT_CONFLICT','explicit auth external URL conflicts with the owned requester origin');
|
|
16
|
+
Object.defineProperty(auth,'external_url',{value:target,writable:true,enumerable:true,configurable:true});
|
|
17
|
+
let text,roundtrip;try{text=stringify(document,{numbersAsFloat:true,strictTemporal:true});roundtrip=parse(text,{integersAsBigInt:true,unsafeKeyBehaviour:'throw'})}catch{fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config could not roundtrip safely')}
|
|
18
|
+
if(!isDeepStrictEqual(roundtrip,document))fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config changed during TOML roundtrip');
|
|
19
|
+
return text;
|
|
20
|
+
}catch(error){if(error.code?.startsWith('REQUESTER_ORIGIN_'))throw error;fail('REQUESTER_ORIGIN_PARSER_UNAVAILABLE','vendored requester-origin TOML parser unavailable')}
|
|
21
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// BOT-2780: one requester listener, reserved before a service generates URLs.
|
|
2
|
+
// The CLI later binds it to exactly one API port on its owned SSH tunnel. This
|
|
3
|
+
// listener is not itself an SSH forward and never supplies forward attestation.
|
|
4
|
+
import {createServer,connect} from 'node:net';
|
|
5
|
+
|
|
6
|
+
const fail=(code,message)=>{const error=Error(message);error.code=code;throw error};
|
|
7
|
+
const validPort=value=>Number.isInteger(value)&&value>=1024&&value<=65535;
|
|
8
|
+
const identity=value=>typeof value==='string'&&value.length>0&&value.length<=4096&&!/[\x00-\x1f\x7f]/.test(value);
|
|
9
|
+
function contextSnapshot(value){
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||!Object.isFrozen(value))fail('REQUESTER_ORIGIN_CONTEXT_REFUSED','immutable requester context required');
|
|
11
|
+
const keys=Object.keys(value).sort();if(!keys.length||keys.length>16||keys.some(key=>!identity(key)||!identity(value[key])))fail('REQUESTER_ORIGIN_CONTEXT_REFUSED','requester context refused');
|
|
12
|
+
return JSON.stringify(keys.map(key=>[key,value[key]]));
|
|
13
|
+
}
|
|
14
|
+
function apiPort(value){
|
|
15
|
+
let url;try{url=new URL(value)}catch{fail('REQUESTER_ORIGIN_FORWARD_REFUSED','owned API URL unavailable')}
|
|
16
|
+
if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password||url.pathname!=='/'||url.search||url.hash||!validPort(Number(url.port)))fail('REQUESTER_ORIGIN_FORWARD_REFUSED','API target must be the owned loopback HTTP endpoint');
|
|
17
|
+
return Number(url.port);
|
|
18
|
+
}
|
|
19
|
+
export async function reserveRequesterOrigin({context,port=0}={}){
|
|
20
|
+
const snapshot=contextSnapshot(context);if(port!==0&&!validPort(port))fail('REQUESTER_ORIGIN_PORT_REFUSED','requester origin port refused');
|
|
21
|
+
const sockets=new Set(),lossListeners=new Set();let route,closed=false,lostCode,closePromise;
|
|
22
|
+
const live=()=>{try{return !closed&&route?.isLive()===true}catch{return false}};
|
|
23
|
+
const close=code=>{
|
|
24
|
+
if(closePromise)return closePromise;closed=true;lostCode=code??lostCode;
|
|
25
|
+
for(const socket of sockets)socket.destroy();sockets.clear();
|
|
26
|
+
closePromise=new Promise(resolve=>server.close(()=>resolve()));
|
|
27
|
+
if(code)for(const listener of lossListeners){try{listener({code})}catch{}}
|
|
28
|
+
return closePromise;
|
|
29
|
+
};
|
|
30
|
+
const server=createServer({pauseOnConnect:true},socket=>{
|
|
31
|
+
if(!route){socket.destroy();return}
|
|
32
|
+
if(!live()){socket.destroy();void close('REQUESTER_ORIGIN_TUNNEL_LOST');return}
|
|
33
|
+
sockets.add(socket);const upstream=connect({host:'127.0.0.1',port:route.targetPort});sockets.add(upstream);
|
|
34
|
+
socket.once('error',()=>upstream.destroy());socket.once('close',()=>{sockets.delete(socket);upstream.destroy()});upstream.once('close',()=>sockets.delete(upstream));
|
|
35
|
+
upstream.once('error',()=>{socket.destroy();void close('REQUESTER_ORIGIN_FORWARD_LOST')});
|
|
36
|
+
upstream.once('connect',()=>{
|
|
37
|
+
if(!live()){socket.destroy();upstream.destroy();void close('REQUESTER_ORIGIN_TUNNEL_LOST');return}
|
|
38
|
+
// Pipe only after rechecking custody at connect time. Request paths, auth,
|
|
39
|
+
// bodies and WebSocket upgrade bytes are transparent; there is no URL target.
|
|
40
|
+
socket.pipe(upstream);upstream.pipe(socket);socket.resume();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
server.maxConnections=64;
|
|
44
|
+
try{await new Promise((resolve,reject)=>{server.once('error',reject);server.listen({host:'127.0.0.1',port,exclusive:true},resolve)})}catch(error){await close();fail(error.code==='EADDRINUSE'?'REQUESTER_ORIGIN_PORT_BUSY':'REQUESTER_ORIGIN_BIND_FAILED','requester origin could not be reserved')}
|
|
45
|
+
const boundPort=server.address().port;server.on('error',()=>{void close('REQUESTER_ORIGIN_BIND_FAILED')});
|
|
46
|
+
return{
|
|
47
|
+
origin:'http://127.0.0.1:'+boundPort,
|
|
48
|
+
activate({context:current,leaseId,projectId,sshTarget,remoteConnection,forwardedConnection,forwards,isLive,watchLoss}={}){
|
|
49
|
+
if(closed||route)fail('REQUESTER_ORIGIN_STATE_REFUSED','requester origin cannot be rebound in place');
|
|
50
|
+
if(contextSnapshot(current)!==snapshot||![leaseId,projectId,sshTarget].every(identity)||typeof isLive!=='function'||typeof watchLoss!=='function')fail('REQUESTER_ORIGIN_CONTEXT_REFUSED','owned current lease/forward context required');
|
|
51
|
+
const remote=apiPort(remoteConnection?.api_url),local=apiPort(forwardedConnection?.api_url);
|
|
52
|
+
if(!Array.isArray(forwards)||forwards.length<1||forwards.length>32||forwards.some(row=>!identity(row?.name)||!validPort(row.remote)||!validPort(row.local))||['name','remote','local'].some(key=>new Set(forwards.map(row=>row[key])).size!==forwards.length))fail('REQUESTER_ORIGIN_FORWARD_REFUSED','SSH forward table refused');
|
|
53
|
+
const api=forwards.find(row=>row.remote===remote);if(!api||api.local!==local||local===boundPort)fail('REQUESTER_ORIGIN_FORWARD_REFUSED','API URL differs from actual SSH forward');
|
|
54
|
+
if(isLive()!==true)fail('REQUESTER_ORIGIN_TUNNEL_LOST','owned SSH tunnel is not live');
|
|
55
|
+
route=Object.freeze({leaseId,projectId,sshTarget,targetPort:local,isLive});
|
|
56
|
+
try{watchLoss(()=>{void close('REQUESTER_ORIGIN_TUNNEL_LOST')})}catch{void close('REQUESTER_ORIGIN_TUNNEL_LOST');fail('REQUESTER_ORIGIN_TUNNEL_LOST','owned SSH loss subscription unavailable')}
|
|
57
|
+
if(!live()){void close('REQUESTER_ORIGIN_TUNNEL_LOST');fail('REQUESTER_ORIGIN_TUNNEL_LOST','owned SSH tunnel ended during activation')}
|
|
58
|
+
},
|
|
59
|
+
close:()=>close(),isLive:live,
|
|
60
|
+
onLost(listener){lossListeners.add(listener);if(lostCode)listener({code:lostCode})},
|
|
61
|
+
};
|
|
62
|
+
}
|