@skrr-ai/cli 0.1.80 → 0.1.81
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/base-command.d.ts +4 -0
- package/dist/base-command.js +31 -2
- package/dist/commands/agents/visibility.d.ts +14 -0
- package/dist/commands/agents/visibility.js +52 -0
- package/dist/commands/create-token.d.ts +0 -1
- package/dist/commands/create-token.js +3 -1
- package/dist/commands/list-tokens.d.ts +0 -1
- package/dist/commands/list-tokens.js +1 -1
- package/dist/commands/machines/dedicated/ssh-proxy.js +1 -0
- package/dist/commands/revoke-token.d.ts +0 -1
- package/dist/commands/revoke-token.js +1 -1
- package/dist/commands/store/follow.d.ts +1 -0
- package/dist/commands/store/follow.js +7 -1
- package/dist/commands/store/follows/list.d.ts +11 -0
- package/dist/commands/store/follows/list.js +29 -0
- package/dist/commands/store/follows/remove.d.ts +16 -0
- package/dist/commands/store/follows/remove.js +37 -0
- package/dist/commands/store/publishers/profile/draft.d.ts +17 -0
- package/dist/commands/store/publishers/profile/draft.js +54 -0
- package/dist/commands/store/publishers/profile/handle.d.ts +14 -0
- package/dist/commands/store/publishers/profile/handle.js +39 -0
- package/dist/commands/store/publishers/profile/hide.d.ts +12 -0
- package/dist/commands/store/publishers/profile/hide.js +39 -0
- package/dist/commands/store/publishers/profile/preview.d.ts +11 -0
- package/dist/commands/store/publishers/profile/preview.js +39 -0
- package/dist/commands/store/publishers/profile/publish.d.ts +12 -0
- package/dist/commands/store/publishers/profile/publish.js +41 -0
- package/dist/commands/store/publishers/profile/restore.d.ts +15 -0
- package/dist/commands/store/publishers/profile/restore.js +49 -0
- package/dist/commands/store/publishers/profile/revisions.d.ts +11 -0
- package/dist/commands/store/publishers/profile/revisions.js +30 -0
- package/dist/commands/store/publishers/profile/show.d.ts +11 -0
- package/dist/commands/store/publishers/profile/show.js +39 -0
- package/dist/commands/store/review.d.ts +26 -0
- package/dist/commands/store/review.js +93 -0
- package/dist/commands/store/reviews.d.ts +29 -0
- package/dist/commands/store/reviews.js +170 -0
- package/dist/commands/store/tip.d.ts +20 -0
- package/dist/commands/store/tip.js +57 -0
- package/dist/commands/tokens/create.d.ts +28 -0
- package/dist/commands/tokens/create.js +136 -0
- package/dist/commands/tokens/list.d.ts +11 -0
- package/dist/commands/tokens/list.js +53 -0
- package/dist/commands/tokens/revoke.d.ts +14 -0
- package/dist/commands/tokens/revoke.js +42 -0
- package/dist/lib/access-tokens.d.ts +59 -0
- package/dist/lib/access-tokens.js +50 -0
- package/dist/lib/auth-core-init.d.ts +6 -0
- package/dist/lib/auth-core-init.js +28 -6
- package/dist/lib/credential-resolver.d.ts +1 -1
- package/dist/lib/credential-resolver.js +8 -2
- package/dist/lib/daemonBroker.d.ts +13 -2
- package/dist/lib/daemonBroker.js +6 -4
- package/dist/lib/dedicated-ssh.d.ts +9 -0
- package/dist/lib/dedicated-ssh.js +232 -63
- package/dist/lib/node-adapter.js +2 -2
- package/dist/lib/publisher-profile-flags.d.ts +19 -0
- package/dist/lib/publisher-profile-flags.js +48 -0
- package/dist/lib/refresh.d.ts +1 -1
- package/dist/lib/refresh.js +6 -1
- package/dist/lib/ssh-ordered-bytes.d.ts +8 -0
- package/dist/lib/ssh-ordered-bytes.js +31 -0
- package/dist/lib/usd-micros.d.ts +9 -0
- package/dist/lib/usd-micros.js +21 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +22 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +29 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +22 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +27 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3857 -3859
- package/oclif.manifest.json +38008 -36666
- package/package.json +28 -4
|
@@ -23,6 +23,7 @@ const socket_io_client_1 = require("socket.io-client");
|
|
|
23
23
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
24
24
|
const agentic_stream_1 = require("./agentic-stream");
|
|
25
25
|
const dpop_auth_1 = require("./dpop-auth");
|
|
26
|
+
const ssh_ordered_bytes_1 = require("./ssh-ordered-bytes");
|
|
26
27
|
/**
|
|
27
28
|
* Owner SSH to a Dedicated Runtime — SSH protocol over the admitted relay.
|
|
28
29
|
*
|
|
@@ -278,48 +279,103 @@ function describeSshStartRefusal(ack) {
|
|
|
278
279
|
*/
|
|
279
280
|
function runSshProxy(options) {
|
|
280
281
|
const channelId = `ssh-${(0, node_crypto_1.randomUUID)()}`;
|
|
282
|
+
const resumeProof = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
281
283
|
const connect = options.connect ?? socket_io_client_1.io;
|
|
282
284
|
const { input, output, errorOutput } = options;
|
|
283
285
|
const identity = ensureSshIdentity();
|
|
284
286
|
const writeHosts = options.writeKnownHosts ?? writeKnownHosts;
|
|
287
|
+
const recoveryMs = options.recoveryMs ?? 45_000;
|
|
288
|
+
const receiver = new ssh_ordered_bytes_1.SshOrderedBytes();
|
|
285
289
|
return new Promise((resolve) => {
|
|
286
290
|
let started = false;
|
|
291
|
+
let startSent = false;
|
|
287
292
|
let finished = false;
|
|
293
|
+
let online = false;
|
|
294
|
+
let attached = false;
|
|
295
|
+
let resumable = false;
|
|
296
|
+
let generation = null;
|
|
297
|
+
let epoch = (0, node_crypto_1.randomUUID)();
|
|
298
|
+
let recoveringSince = null;
|
|
299
|
+
let inputOffset = 0;
|
|
300
|
+
let attachmentSeq = 0;
|
|
301
|
+
let closing = false;
|
|
302
|
+
let lastKeepaliveAt = 0;
|
|
303
|
+
let pendingInput = null;
|
|
304
|
+
const identityAuth = () => ({ ...(0, data_provider_1.getClientIdentityAuth)(), clientPlatform: 'cli',
|
|
305
|
+
clientOs: process.platform === 'darwin' ? 'macos' : process.platform });
|
|
288
306
|
const wsOrigin = (0, agentic_stream_1.toWebSocketOrigin)(options.baseURL);
|
|
307
|
+
const handshakeAuth = (token, callback) => {
|
|
308
|
+
// Resolve a fresh proof after token renewal on every reconnect. Never
|
|
309
|
+
// reuse the original handoff token or its single-use DPoP proof.
|
|
310
|
+
const auth = (0, dpop_auth_1.socketAuthWithDpop)({ token, ...identityAuth() }, wsOrigin);
|
|
311
|
+
if (typeof auth === 'function')
|
|
312
|
+
auth(callback);
|
|
313
|
+
else
|
|
314
|
+
callback(auth);
|
|
315
|
+
};
|
|
289
316
|
const socket = connect(wsOrigin, {
|
|
290
317
|
path: '/ws/agentic',
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
transports: ['websocket'],
|
|
299
|
-
// A reconnect cannot resume a channel whose admission line the daemon
|
|
300
|
-
// revoked on socket death — say it ended and let ssh retry fresh.
|
|
301
|
-
reconnection: false,
|
|
318
|
+
auth: options.getToken ? (callback) => {
|
|
319
|
+
void options.getToken().then((token) => handshakeAuth(token, callback), () => {
|
|
320
|
+
finish(1, 'Could not renew SSH authentication. Restore skrr login and connect again.');
|
|
321
|
+
});
|
|
322
|
+
} : (0, dpop_auth_1.socketAuthWithDpop)({ token: options.token, ...identityAuth() }, wsOrigin),
|
|
323
|
+
transports: ['websocket'], reconnection: true,
|
|
324
|
+
reconnectionDelay: 1000, reconnectionDelayMax: 2000,
|
|
302
325
|
});
|
|
303
|
-
const startTimer = setTimeout(() =>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
326
|
+
const startTimer = setTimeout(() => finish(1, 'The machine did not answer the SSH channel in time. Its daemon may be busy or offline.'), exports.SSH_START_TIMEOUT_MS);
|
|
327
|
+
function sendPendingInput() {
|
|
328
|
+
if (closing || !online || !attached || !pendingInput || finished)
|
|
329
|
+
return;
|
|
330
|
+
socket.emit('dedicated:ssh:data', { channelId, data: pendingInput.bytes.toString('base64'),
|
|
331
|
+
resumeV2: true, epoch, generation, offset: pendingInput.offset });
|
|
332
|
+
}
|
|
333
|
+
function pumpInput() {
|
|
334
|
+
if (closing || !started || !resumable || !online || !attached || pendingInput || finished)
|
|
335
|
+
return;
|
|
336
|
+
// Explicit reads bound retained input even if ssh writes a large buffer.
|
|
337
|
+
const available = input.readableLength;
|
|
338
|
+
if (!available)
|
|
339
|
+
return;
|
|
340
|
+
const bytes = input.read(Math.min(16 * 1024, available));
|
|
341
|
+
if (!bytes?.length)
|
|
342
|
+
return;
|
|
343
|
+
pendingInput = { bytes: Buffer.from(bytes), offset: inputOffset, firstSentAt: Date.now() };
|
|
344
|
+
sendPendingInput();
|
|
345
|
+
}
|
|
346
|
+
const onLegacyInput = (chunk) => {
|
|
347
|
+
if (!online || finished)
|
|
348
|
+
return;
|
|
307
349
|
const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
308
350
|
socket.emit('dedicated:ssh:data', { channelId, data: bytes.toString('base64') });
|
|
309
351
|
};
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
352
|
+
function sendResume() {
|
|
353
|
+
if (online && started && resumable && !attached && !finished) {
|
|
354
|
+
socket.emit('dedicated:ssh:resume', { leaseId: options.leaseId, channelId, resumeProof, epoch, generation, attachmentSeq });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function keepalive() {
|
|
358
|
+
if (started && resumable && attached && online && !finished && Date.now() - lastKeepaliveAt >= 10_000) {
|
|
359
|
+
lastKeepaliveAt = Date.now();
|
|
360
|
+
socket.emit('dedicated:ssh:keepalive', { channelId, epoch });
|
|
361
|
+
}
|
|
317
362
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
363
|
+
const ticker = setInterval(() => {
|
|
364
|
+
if (finished)
|
|
365
|
+
return;
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
if ((recoveringSince !== null && now - recoveringSince >= recoveryMs) ||
|
|
368
|
+
(pendingInput && now - pendingInput.firstSentAt >= recoveryMs)) {
|
|
369
|
+
finish(1, 'SSH connection recovery timed out; connect again.');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!online && started && resumable && socket.active === false)
|
|
373
|
+
socket.connect();
|
|
374
|
+
sendResume();
|
|
375
|
+
sendPendingInput();
|
|
376
|
+
keepalive();
|
|
377
|
+
}, 2000);
|
|
378
|
+
ticker.unref?.();
|
|
323
379
|
const onInputEnded = () => finish(0);
|
|
324
380
|
const onOutputError = () => finish(1);
|
|
325
381
|
input.once('end', onInputEnded);
|
|
@@ -329,25 +385,44 @@ function runSshProxy(options) {
|
|
|
329
385
|
if (finished)
|
|
330
386
|
return;
|
|
331
387
|
finished = true;
|
|
332
|
-
|
|
388
|
+
clearTimeout(startTimer);
|
|
389
|
+
clearInterval(ticker);
|
|
390
|
+
input.off('data', onLegacyInput);
|
|
391
|
+
input.off('readable', pumpInput);
|
|
392
|
+
input.off('end', onInputEnded);
|
|
393
|
+
input.off('close', onInputEnded);
|
|
394
|
+
output.off('error', onOutputError);
|
|
395
|
+
input.pause();
|
|
396
|
+
pendingInput = null;
|
|
333
397
|
if (notice)
|
|
334
398
|
errorOutput.write(`${notice}\n`);
|
|
335
|
-
|
|
336
|
-
|
|
399
|
+
// Never queue an offline stop that might arrive after another attachment.
|
|
400
|
+
if (started && online)
|
|
401
|
+
socket.emit('dedicated:ssh:stop', { channelId, ...(resumable ? { epoch } : {}) });
|
|
337
402
|
socket.disconnect();
|
|
338
403
|
resolve(code);
|
|
339
404
|
}
|
|
340
|
-
socket.on('connect_error', (
|
|
341
|
-
|
|
405
|
+
socket.on('connect_error', (error) => {
|
|
406
|
+
if (finished || (started && resumable))
|
|
407
|
+
return;
|
|
408
|
+
finish(1, `Could not connect to skrr: ${error.message}`);
|
|
342
409
|
});
|
|
343
410
|
socket.on('connect', () => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
411
|
+
if (finished)
|
|
412
|
+
return;
|
|
413
|
+
online = true;
|
|
414
|
+
if (started) {
|
|
415
|
+
sendResume();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (startSent) {
|
|
419
|
+
finish(1, 'SSH admission was interrupted before confirmation; connect again.');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
startSent = true;
|
|
423
|
+
socket.emit('dedicated:ssh:start', { leaseId: options.leaseId, channelId,
|
|
424
|
+
keyType: identity.keyType, keyBlob: identity.keyBlob, receiverAckV1: true,
|
|
425
|
+
resumeV2: true, resumeProof, epoch });
|
|
351
426
|
});
|
|
352
427
|
socket.on('dedicated:ssh:start:ack', (ack) => {
|
|
353
428
|
if (!ack || ack.channelId !== channelId || started || finished)
|
|
@@ -356,48 +431,142 @@ function runSshProxy(options) {
|
|
|
356
431
|
finish(1, describeSshStartRefusal(ack));
|
|
357
432
|
return;
|
|
358
433
|
}
|
|
434
|
+
if (ack.resumeV2 === true) {
|
|
435
|
+
if (ack.epoch !== epoch || !Number.isSafeInteger(ack.generation) || Number(ack.generation) < 1 ||
|
|
436
|
+
(generation !== null && generation !== ack.generation)) {
|
|
437
|
+
finish(1, 'Invalid SSH stream admission.');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
resumable = true;
|
|
441
|
+
generation = ack.generation;
|
|
442
|
+
}
|
|
359
443
|
started = true;
|
|
444
|
+
attached = true;
|
|
360
445
|
clearTimeout(startTimer);
|
|
361
|
-
// The instance's keys, attested by the channel — write them before the
|
|
362
|
-
// first protocol byte flows so ssh's host-key check reads the new file.
|
|
363
|
-
// `host` is what ssh asks about (`%h`), so the pin goes under the alias;
|
|
364
|
-
// a daemon that reported no keys becomes an honest StrictHostKeyChecking
|
|
365
|
-
// refusal, never a silent TOFU.
|
|
366
446
|
const host = `${options.leaseId}${exports.SSH_HOST_SUFFIX}`;
|
|
367
|
-
if (!writeHosts(host, ack.hostKeys))
|
|
447
|
+
if (!writeHosts(host, ack.hostKeys))
|
|
368
448
|
errorOutput.write('[skrr ssh] warning: the machine reported no host keys; the connection will be refused if none are pinned\n');
|
|
449
|
+
if (resumable) {
|
|
450
|
+
input.pause();
|
|
451
|
+
input.on('readable', pumpInput);
|
|
452
|
+
keepalive();
|
|
453
|
+
pumpInput();
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
input.on('data', onLegacyInput);
|
|
457
|
+
input.resume();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
socket.on('dedicated:ssh:resume:ack', (ack) => {
|
|
461
|
+
if (!ack || ack.channelId !== channelId || ack.epoch !== epoch || !started || !resumable || finished || attached)
|
|
462
|
+
return;
|
|
463
|
+
if (!ack.ok) {
|
|
464
|
+
if (ack.retryable === true)
|
|
465
|
+
return;
|
|
466
|
+
finish(1, ack.error || 'The existing SSH channel cannot resume.');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (ack.resumeV2 !== true || ack.generation !== generation) {
|
|
470
|
+
finish(1, 'Invalid SSH resume admission.');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (ack.closed === true) {
|
|
474
|
+
finish(ack.failed === true ? 1 : 0);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
closing = ack.closing === true;
|
|
478
|
+
attached = true;
|
|
479
|
+
recoveringSince = null;
|
|
480
|
+
lastKeepaliveAt = 0;
|
|
481
|
+
keepalive();
|
|
482
|
+
sendPendingInput();
|
|
483
|
+
pumpInput();
|
|
484
|
+
});
|
|
485
|
+
socket.on('dedicated:ssh:input:ack', (ack) => {
|
|
486
|
+
if (finished || !resumable || !pendingInput || !attached || ack?.channelId !== channelId ||
|
|
487
|
+
ack.epoch !== epoch || ack.generation !== generation || ack.offset !== pendingInput.offset)
|
|
488
|
+
return;
|
|
489
|
+
if (!ack.ok) {
|
|
490
|
+
finish(1, ack.error || 'The SSH input stream was refused.');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (ack.nextOffset !== pendingInput.offset + pendingInput.bytes.length) {
|
|
494
|
+
finish(1, 'Invalid SSH input acknowledgment.');
|
|
495
|
+
return;
|
|
369
496
|
}
|
|
370
|
-
|
|
371
|
-
|
|
497
|
+
inputOffset = ack.nextOffset;
|
|
498
|
+
pendingInput = null;
|
|
499
|
+
pumpInput();
|
|
372
500
|
});
|
|
373
|
-
socket.on('dedicated:ssh:data', (frame, receipt) => {
|
|
374
|
-
if (!frame || frame.channelId !== channelId || finished)
|
|
501
|
+
socket.on('dedicated:ssh:data', async (frame, receipt) => {
|
|
502
|
+
if (!frame || frame.channelId !== channelId || finished || typeof frame.data !== 'string' || !frame.data.length)
|
|
375
503
|
return;
|
|
376
|
-
|
|
504
|
+
try {
|
|
377
505
|
const bytes = Buffer.from(frame.data, 'base64');
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
506
|
+
if (frame.resumeV2 === true) {
|
|
507
|
+
if (frame.epoch !== epoch)
|
|
508
|
+
return;
|
|
509
|
+
if (!Number.isSafeInteger(frame.generation) || Number(frame.generation) < 1 ||
|
|
510
|
+
(generation !== null && frame.generation !== generation) || bytes.toString('base64') !== frame.data) {
|
|
511
|
+
throw new Error('invalid SSH output frame');
|
|
512
|
+
}
|
|
513
|
+
generation ??= frame.generation;
|
|
514
|
+
const nextOffset = await receiver.receive(frame.offset, bytes, (value) => new Promise((done, reject) => output.write(value, (error) => error ? reject(error) : done())));
|
|
515
|
+
receipt?.({ channelId, ok: !finished, epoch: frame.epoch, nextOffset });
|
|
386
516
|
}
|
|
387
517
|
else {
|
|
388
|
-
|
|
518
|
+
if (resumable)
|
|
519
|
+
throw new Error('unsequenced output on resumable SSH stream');
|
|
520
|
+
if (typeof receipt === 'function') {
|
|
521
|
+
output.write(bytes, (error) => {
|
|
522
|
+
receipt({ channelId, ok: !error && !finished });
|
|
523
|
+
if (error)
|
|
524
|
+
finish(1, 'Could not write SSH output.');
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
output.write(bytes);
|
|
389
529
|
}
|
|
390
530
|
}
|
|
531
|
+
catch {
|
|
532
|
+
receipt?.({ channelId, ok: false, epoch: frame.epoch });
|
|
533
|
+
finish(1, 'Could not preserve the SSH output stream.');
|
|
534
|
+
}
|
|
391
535
|
});
|
|
392
|
-
socket.on('dedicated:ssh:closed', (frame) => {
|
|
393
|
-
if (!frame || frame.channelId !== channelId)
|
|
536
|
+
socket.on('dedicated:ssh:closed', (frame, receipt) => {
|
|
537
|
+
if (!frame || frame.channelId !== channelId || (frame.epoch && frame.epoch !== epoch))
|
|
394
538
|
return;
|
|
395
|
-
|
|
539
|
+
receipt?.({ channelId, epoch: frame.epoch, ok: true });
|
|
540
|
+
finish(frame.failed === true ? 1 : 0, frame.reason ? `[ssh channel closed: ${frame.reason}]` : undefined);
|
|
541
|
+
});
|
|
542
|
+
function beginRecovery() {
|
|
543
|
+
if (finished || !attached)
|
|
544
|
+
return;
|
|
545
|
+
attached = false;
|
|
546
|
+
input.pause();
|
|
547
|
+
epoch = (0, node_crypto_1.randomUUID)();
|
|
548
|
+
attachmentSeq++;
|
|
549
|
+
if (recoveringSince === null) {
|
|
550
|
+
recoveringSince = Date.now();
|
|
551
|
+
errorOutput.write('[skrr ssh] connection interrupted; restoring the existing SSH stream\n');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
socket.on('dedicated:ssh:lost', (frame) => {
|
|
555
|
+
if (frame?.channelId !== channelId || frame.epoch !== epoch || !resumable || finished)
|
|
556
|
+
return;
|
|
557
|
+
beginRecovery();
|
|
558
|
+
sendResume();
|
|
396
559
|
});
|
|
397
560
|
socket.on('disconnect', () => {
|
|
398
561
|
if (finished)
|
|
399
562
|
return;
|
|
400
|
-
|
|
563
|
+
online = false;
|
|
564
|
+
input.pause();
|
|
565
|
+
if (!started || !resumable) {
|
|
566
|
+
finish(1, '[lost the connection to skrr; the SSH channel has ended]');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
beginRecovery();
|
|
401
570
|
});
|
|
402
571
|
});
|
|
403
572
|
}
|
package/dist/lib/node-adapter.js
CHANGED
|
@@ -202,7 +202,7 @@ function createNodeAdapter(opts = {}) {
|
|
|
202
202
|
return false;
|
|
203
203
|
// Never refresh a CI token — they're static by construction.
|
|
204
204
|
const { token, kind } = currentCredential();
|
|
205
|
-
if (!token ||
|
|
205
|
+
if (!token || (0, auth_core_1.isStaticTokenKind)(kind))
|
|
206
206
|
return false;
|
|
207
207
|
if (refreshInFlight)
|
|
208
208
|
return refreshInFlight;
|
|
@@ -241,7 +241,7 @@ function createNodeAdapter(opts = {}) {
|
|
|
241
241
|
if (process.env.OVERSKY_SKIP_DAEMON_BROKER === '1')
|
|
242
242
|
return false;
|
|
243
243
|
const cred = currentCredential();
|
|
244
|
-
if (cred.kind
|
|
244
|
+
if ((0, auth_core_1.isStaticTokenKind)(cred.kind))
|
|
245
245
|
return false;
|
|
246
246
|
// An operator-supplied `--token` is that operator's explicit
|
|
247
247
|
// authority: when the server refuses it, the honest answer is the
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PublisherProfileDraft } from '@skrr-ai/data-provider';
|
|
2
|
+
/**
|
|
3
|
+
* Shared flags for `skrr store publishers profile *`.
|
|
4
|
+
*
|
|
5
|
+
* `--publish-as` names a workspace you publish for; omitted, the console is
|
|
6
|
+
* your personal publisher profile. The server derives the actual principal —
|
|
7
|
+
* this only selects which of your profiles to address.
|
|
8
|
+
*/
|
|
9
|
+
export declare const publishAsFlag: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
|
+
export declare const jsonFlag: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
/** A profile draft is a document; read it from a JSON file, never from argv. */
|
|
12
|
+
export declare function readDraftFile(file: string): PublisherProfileDraft;
|
|
13
|
+
/**
|
|
14
|
+
* The CAS revision a state change applies to. Publish, hide and restore act
|
|
15
|
+
* on whatever the draft is NOW, so reading it immediately before the write is
|
|
16
|
+
* the intent; a caller holding a specific revision passes it and the server
|
|
17
|
+
* refuses a stale one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function currentRevision(publishAs?: string, explicit?: number): Promise<number>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.jsonFlag = exports.publishAsFlag = void 0;
|
|
4
|
+
exports.readDraftFile = readDraftFile;
|
|
5
|
+
exports.currentRevision = currentRevision;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const core_1 = require("@oclif/core");
|
|
8
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
9
|
+
/**
|
|
10
|
+
* Shared flags for `skrr store publishers profile *`.
|
|
11
|
+
*
|
|
12
|
+
* `--publish-as` names a workspace you publish for; omitted, the console is
|
|
13
|
+
* your personal publisher profile. The server derives the actual principal —
|
|
14
|
+
* this only selects which of your profiles to address.
|
|
15
|
+
*/
|
|
16
|
+
exports.publishAsFlag = core_1.Flags.string({
|
|
17
|
+
description: 'Workspace id to act for (omit for your personal publisher profile)',
|
|
18
|
+
});
|
|
19
|
+
exports.jsonFlag = core_1.Flags.boolean({ description: 'Output as JSON' });
|
|
20
|
+
/** A profile draft is a document; read it from a JSON file, never from argv. */
|
|
21
|
+
function readDraftFile(file) {
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
throw new Error(`Could not read a JSON draft from ${file}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
30
|
+
throw new Error(`${file} must contain a JSON object (the profile draft)`);
|
|
31
|
+
}
|
|
32
|
+
const doc = parsed;
|
|
33
|
+
// Accept either the bare draft or the `profile show --json` envelope's draft.
|
|
34
|
+
const nested = doc.profile?.draft ?? doc.draft;
|
|
35
|
+
return (nested && typeof nested === 'object' ? nested : doc);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The CAS revision a state change applies to. Publish, hide and restore act
|
|
39
|
+
* on whatever the draft is NOW, so reading it immediately before the write is
|
|
40
|
+
* the intent; a caller holding a specific revision passes it and the server
|
|
41
|
+
* refuses a stale one.
|
|
42
|
+
*/
|
|
43
|
+
async function currentRevision(publishAs, explicit) {
|
|
44
|
+
if (explicit !== undefined)
|
|
45
|
+
return explicit;
|
|
46
|
+
const { profile } = await data_provider_1.dataService.getStorePublisherProfileEditor(publishAs);
|
|
47
|
+
return profile.revision;
|
|
48
|
+
}
|
package/dist/lib/refresh.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface EnsureFreshCliCredentialResult {
|
|
|
47
47
|
pair?: TokenPair;
|
|
48
48
|
/** Stable diagnostic code; never contains token or server response text. */
|
|
49
49
|
refreshFailureCode?: string;
|
|
50
|
-
skippedReason?: 'no_token' | 'ci_token' | 'external_source' | 'env_override' | 'bare_mode' | 'fresh' | 'no_refresh_token' | 'refresh_failed';
|
|
50
|
+
skippedReason?: 'no_token' | 'ci_token' | 'personal_access_token' | 'external_source' | 'env_override' | 'bare_mode' | 'fresh' | 'no_refresh_token' | 'refresh_failed';
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Ensure the active CLI credential has enough remaining lifetime for a
|
package/dist/lib/refresh.js
CHANGED
|
@@ -94,7 +94,7 @@ function canMutatePersistedCredential(credential, bareMode) {
|
|
|
94
94
|
return false;
|
|
95
95
|
if (bareMode)
|
|
96
96
|
return false;
|
|
97
|
-
if (credential.kind
|
|
97
|
+
if ((0, auth_core_1.isStaticTokenKind)(credential.kind))
|
|
98
98
|
return false;
|
|
99
99
|
// These sources are operator-owned or generated on demand. Never rotate
|
|
100
100
|
// them into the local keychain/file store behind the operator's back.
|
|
@@ -120,6 +120,11 @@ async function ensureFreshCliCredential(opts = {}) {
|
|
|
120
120
|
if (credential.kind === 'ci') {
|
|
121
121
|
return { credential, refreshed: false, skippedReason: 'ci_token' };
|
|
122
122
|
}
|
|
123
|
+
// A personal access token is static by construction (OSK-13208): there is
|
|
124
|
+
// no refresh family behind it, so there is nothing to rotate.
|
|
125
|
+
if (credential.kind === 'pat') {
|
|
126
|
+
return { credential, refreshed: false, skippedReason: 'personal_access_token' };
|
|
127
|
+
}
|
|
123
128
|
if (!canMutatePersistedCredential(credential, Boolean(opts.bareMode))) {
|
|
124
129
|
return { credential, refreshed: false, skippedReason: 'external_source' };
|
|
125
130
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A bounded receiver for a retryable SSH byte stream. One frame may be
|
|
2
|
+
* outstanding. A lost acknowledgment may repeat that exact frame, never its
|
|
3
|
+
* bytes; an overlapping, changed, or skipped frame is a stream error. */
|
|
4
|
+
export declare class SshOrderedBytes {
|
|
5
|
+
nextOffset: number;
|
|
6
|
+
private last?;
|
|
7
|
+
receive(offset: number, bytes: Buffer, write: (bytes: Buffer) => Promise<void>): Promise<number>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SshOrderedBytes = void 0;
|
|
4
|
+
/** A bounded receiver for a retryable SSH byte stream. One frame may be
|
|
5
|
+
* outstanding. A lost acknowledgment may repeat that exact frame, never its
|
|
6
|
+
* bytes; an overlapping, changed, or skipped frame is a stream error. */
|
|
7
|
+
class SshOrderedBytes {
|
|
8
|
+
nextOffset = 0;
|
|
9
|
+
last;
|
|
10
|
+
receive(offset, bytes, write) {
|
|
11
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || bytes.length < 1 || bytes.length > 16 * 1024 ||
|
|
12
|
+
!Number.isSafeInteger(offset + bytes.length)) {
|
|
13
|
+
return Promise.reject(new Error('invalid SSH stream frame'));
|
|
14
|
+
}
|
|
15
|
+
if (this.last?.offset === offset) {
|
|
16
|
+
return this.last.bytes.equals(bytes)
|
|
17
|
+
? this.last.written
|
|
18
|
+
: Promise.reject(new Error('changed SSH stream replay'));
|
|
19
|
+
}
|
|
20
|
+
if (offset !== this.nextOffset)
|
|
21
|
+
return Promise.reject(new Error('SSH stream offset mismatch'));
|
|
22
|
+
const retained = Buffer.from(bytes);
|
|
23
|
+
const written = Promise.resolve().then(() => write(retained)).then(() => {
|
|
24
|
+
this.nextOffset = offset + retained.length;
|
|
25
|
+
return this.nextOffset;
|
|
26
|
+
});
|
|
27
|
+
this.last = { offset, bytes: retained, written };
|
|
28
|
+
return written;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.SshOrderedBytes = SshOrderedBytes;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a user-typed USD amount ("5", "2.50") into integer micros.
|
|
3
|
+
*
|
|
4
|
+
* Integer arithmetic on the decimal STRING, never `Number(x) * 1e6`: binary
|
|
5
|
+
* floating point turns 0.29 into 289999.99… and a money path that truncates
|
|
6
|
+
* that charges a micro less — or, rounded the other way, a micro more.
|
|
7
|
+
* Returns null for anything that is not a positive amount with ≤ 2 decimals.
|
|
8
|
+
*/
|
|
9
|
+
export declare function usdToMicros(amount: string): number | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usdToMicros = usdToMicros;
|
|
4
|
+
/**
|
|
5
|
+
* Parse a user-typed USD amount ("5", "2.50") into integer micros.
|
|
6
|
+
*
|
|
7
|
+
* Integer arithmetic on the decimal STRING, never `Number(x) * 1e6`: binary
|
|
8
|
+
* floating point turns 0.29 into 289999.99… and a money path that truncates
|
|
9
|
+
* that charges a micro less — or, rounded the other way, a micro more.
|
|
10
|
+
* Returns null for anything that is not a positive amount with ≤ 2 decimals.
|
|
11
|
+
*/
|
|
12
|
+
function usdToMicros(amount) {
|
|
13
|
+
const trimmed = amount.trim();
|
|
14
|
+
if (!/^\d+(\.\d{1,2})?$/.test(trimmed))
|
|
15
|
+
return null;
|
|
16
|
+
const [whole, frac = ''] = trimmed.split('.');
|
|
17
|
+
const cents = Number(whole) * 100 + Number(frac.padEnd(2, '0'));
|
|
18
|
+
if (!Number.isSafeInteger(cents) || cents <= 0)
|
|
19
|
+
return null;
|
|
20
|
+
return cents * 10_000;
|
|
21
|
+
}
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
* Precedence (high → low):
|
|
14
14
|
*
|
|
15
15
|
* 1. flag — `--token <jwt>` CLI flag (debugging)
|
|
16
|
-
* 2. env-token — `OVERSKY_TOKEN`
|
|
17
|
-
*
|
|
16
|
+
* 2. env-token — `OVERSKY_TOKEN` (CI / static). The CLI also reads
|
|
17
|
+
* `SKRR_TOKEN` via `envTokenNames`; `OVERSKY_TOKEN` wins
|
|
18
|
+
* when both are set, matching the CLI config overlay. A
|
|
19
|
+
* personal access token (`osk_pat_*`) is the credential
|
|
20
|
+
* CI should put here.
|
|
18
21
|
* 3. auth-helper — `OVERSKY_AUTH_HELPER` script output (rotating
|
|
19
22
|
* creds — Vault, AWS SM, internal SSO proxy)
|
|
20
23
|
* 4. fd-handoff — `OVERSKY_AUTH_FD` parent→child IPC (Electron
|
|
@@ -64,6 +67,12 @@ export interface CredentialResolverOptions {
|
|
|
64
67
|
* string to use that value directly.
|
|
65
68
|
*/
|
|
66
69
|
envTokenOverride?: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Environment variables read by the env-token slot, in precedence order.
|
|
72
|
+
* Defaults to `['OVERSKY_TOKEN']`. The CLI passes
|
|
73
|
+
* `['OVERSKY_TOKEN', 'SKRR_TOKEN']` (OSK-13208).
|
|
74
|
+
*/
|
|
75
|
+
envTokenNames?: readonly string[];
|
|
67
76
|
/** Test/explicit override for `OVERSKY_AUTH_HELPER` env. Same semantics. */
|
|
68
77
|
authHelperPathOverride?: string | null;
|
|
69
78
|
/**
|
|
@@ -143,6 +152,8 @@ export interface AuthHelperSentinelInfo {
|
|
|
143
152
|
* (POST /api/daemons/token), MUST NOT schedule refresh,
|
|
144
153
|
* and on auth failure exits 75 with a CI-specific error
|
|
145
154
|
* rather than writing needs-reauth.json.
|
|
155
|
+
* - 'pat' — personal access token (`osk_pat_*`, OSK-13208): a static
|
|
156
|
+
* bearer used AS the user by the CLI. Never refreshed.
|
|
146
157
|
* - 'install' — short-lived web-minted JWT with `scope=daemon-install`.
|
|
147
158
|
* The daemon exchanges it through the same /api/daemons/token
|
|
148
159
|
* bootstrap path, persists only the returned daemon
|
|
@@ -157,9 +168,17 @@ export interface AuthHelperSentinelInfo {
|
|
|
157
168
|
* The kill switch `OVERSKY_DISABLE_CI_TOKEN_RESOLVER=1` forces every token to
|
|
158
169
|
* `'standard'` for triage purposes.
|
|
159
170
|
*/
|
|
160
|
-
export type TokenKind = 'standard' | 'ci' | 'install';
|
|
171
|
+
export type TokenKind = 'standard' | 'ci' | 'install' | 'pat';
|
|
161
172
|
/** CI-token plaintext prefix; mirrors api/server/services/Auth/daemonCiTokens.js. */
|
|
162
173
|
export declare const CI_TOKEN_PREFIX = "osk_ci_";
|
|
174
|
+
/** Personal-access-token prefix; mirrors api/server/services/Auth/personalAccessTokens.js. */
|
|
175
|
+
export declare const PERSONAL_ACCESS_TOKEN_PREFIX = "osk_pat_";
|
|
176
|
+
/**
|
|
177
|
+
* True for a kind whose token is static by construction: never refreshed,
|
|
178
|
+
* never rotated into a store. Like `TokenKind` itself this selects a local
|
|
179
|
+
* code path and grants nothing.
|
|
180
|
+
*/
|
|
181
|
+
export declare function isStaticTokenKind(kind: TokenKind): boolean;
|
|
163
182
|
/**
|
|
164
183
|
* Classify a token by local shape. Pure function; safe to call on any string
|
|
165
184
|
* (including `null`-coerced empty). Returns `'standard'` for empty input
|