@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.
Files changed (80) hide show
  1. package/dist/base-command.d.ts +4 -0
  2. package/dist/base-command.js +31 -2
  3. package/dist/commands/agents/visibility.d.ts +14 -0
  4. package/dist/commands/agents/visibility.js +52 -0
  5. package/dist/commands/create-token.d.ts +0 -1
  6. package/dist/commands/create-token.js +3 -1
  7. package/dist/commands/list-tokens.d.ts +0 -1
  8. package/dist/commands/list-tokens.js +1 -1
  9. package/dist/commands/machines/dedicated/ssh-proxy.js +1 -0
  10. package/dist/commands/revoke-token.d.ts +0 -1
  11. package/dist/commands/revoke-token.js +1 -1
  12. package/dist/commands/store/follow.d.ts +1 -0
  13. package/dist/commands/store/follow.js +7 -1
  14. package/dist/commands/store/follows/list.d.ts +11 -0
  15. package/dist/commands/store/follows/list.js +29 -0
  16. package/dist/commands/store/follows/remove.d.ts +16 -0
  17. package/dist/commands/store/follows/remove.js +37 -0
  18. package/dist/commands/store/publishers/profile/draft.d.ts +17 -0
  19. package/dist/commands/store/publishers/profile/draft.js +54 -0
  20. package/dist/commands/store/publishers/profile/handle.d.ts +14 -0
  21. package/dist/commands/store/publishers/profile/handle.js +39 -0
  22. package/dist/commands/store/publishers/profile/hide.d.ts +12 -0
  23. package/dist/commands/store/publishers/profile/hide.js +39 -0
  24. package/dist/commands/store/publishers/profile/preview.d.ts +11 -0
  25. package/dist/commands/store/publishers/profile/preview.js +39 -0
  26. package/dist/commands/store/publishers/profile/publish.d.ts +12 -0
  27. package/dist/commands/store/publishers/profile/publish.js +41 -0
  28. package/dist/commands/store/publishers/profile/restore.d.ts +15 -0
  29. package/dist/commands/store/publishers/profile/restore.js +49 -0
  30. package/dist/commands/store/publishers/profile/revisions.d.ts +11 -0
  31. package/dist/commands/store/publishers/profile/revisions.js +30 -0
  32. package/dist/commands/store/publishers/profile/show.d.ts +11 -0
  33. package/dist/commands/store/publishers/profile/show.js +39 -0
  34. package/dist/commands/store/review.d.ts +26 -0
  35. package/dist/commands/store/review.js +93 -0
  36. package/dist/commands/store/reviews.d.ts +29 -0
  37. package/dist/commands/store/reviews.js +170 -0
  38. package/dist/commands/store/tip.d.ts +20 -0
  39. package/dist/commands/store/tip.js +57 -0
  40. package/dist/commands/tokens/create.d.ts +28 -0
  41. package/dist/commands/tokens/create.js +136 -0
  42. package/dist/commands/tokens/list.d.ts +11 -0
  43. package/dist/commands/tokens/list.js +53 -0
  44. package/dist/commands/tokens/revoke.d.ts +14 -0
  45. package/dist/commands/tokens/revoke.js +42 -0
  46. package/dist/lib/access-tokens.d.ts +59 -0
  47. package/dist/lib/access-tokens.js +50 -0
  48. package/dist/lib/auth-core-init.d.ts +6 -0
  49. package/dist/lib/auth-core-init.js +28 -6
  50. package/dist/lib/credential-resolver.d.ts +1 -1
  51. package/dist/lib/credential-resolver.js +8 -2
  52. package/dist/lib/daemonBroker.d.ts +13 -2
  53. package/dist/lib/daemonBroker.js +6 -4
  54. package/dist/lib/dedicated-ssh.d.ts +9 -0
  55. package/dist/lib/dedicated-ssh.js +232 -63
  56. package/dist/lib/node-adapter.js +2 -2
  57. package/dist/lib/publisher-profile-flags.d.ts +19 -0
  58. package/dist/lib/publisher-profile-flags.js +48 -0
  59. package/dist/lib/refresh.d.ts +1 -1
  60. package/dist/lib/refresh.js +6 -1
  61. package/dist/lib/ssh-ordered-bytes.d.ts +8 -0
  62. package/dist/lib/ssh-ordered-bytes.js +31 -0
  63. package/dist/lib/usd-micros.d.ts +9 -0
  64. package/dist/lib/usd-micros.js +21 -0
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +22 -3
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +29 -5
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -1
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +7 -1
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -5
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +22 -3
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +27 -4
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -1
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +7 -1
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  77. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  78. package/dist/node_modules/@skrr-ai/data-provider/index.js +3857 -3859
  79. package/oclif.manifest.json +38008 -36666
  80. 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
- // OSK-12020 — a DPoP-bound token proves possession on every handshake.
292
- auth: (0, dpop_auth_1.socketAuthWithDpop)({
293
- token: options.token,
294
- ...(0, data_provider_1.getClientIdentityAuth)(),
295
- clientPlatform: 'cli',
296
- clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
297
- }, wsOrigin),
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
- finish(1, 'The machine did not answer the SSH channel in time. Its daemon may be busy or offline.');
305
- }, exports.SSH_START_TIMEOUT_MS);
306
- const onInput = (chunk) => {
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 cleanup() {
311
- clearTimeout(startTimer);
312
- input.off('data', onInput);
313
- input.off('end', onInputEnded);
314
- input.off('close', onInputEnded);
315
- output.off('error', onOutputError);
316
- input.pause();
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
- // The ssh client exiting is visible ONLY as stdin EOF and an EPIPE on
319
- // stdout — sshd cannot tell the ProxyCommand died until its socket sees
320
- // the stop or the transport drops. Listen for both so the channel, and
321
- // the daemon's key admission, end when ssh does instead of at idle
322
- // timeout.
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
- cleanup();
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
- if (started)
336
- socket.emit('dedicated:ssh:stop', { channelId });
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', (err) => {
341
- finish(1, `Could not connect to skrr: ${err.message}`);
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
- socket.emit('dedicated:ssh:start', {
345
- leaseId: options.leaseId,
346
- channelId,
347
- keyType: identity.keyType,
348
- keyBlob: identity.keyBlob,
349
- receiverAckV1: true,
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
- input.resume();
371
- input.on('data', onInput);
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
- if (typeof frame.data === 'string' && frame.data.length > 0) {
504
+ try {
377
505
  const bytes = Buffer.from(frame.data, 'base64');
378
- if (typeof receipt === 'function') {
379
- // A frame can precede start-ack (sshd's banner). The owned channel
380
- // already advertised support; confirm only when stdout flushed.
381
- output.write(bytes, (error) => {
382
- receipt({ channelId, ok: !error && !finished });
383
- if (error)
384
- finish(1, 'Could not write SSH output.');
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
- output.write(bytes);
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
- finish(0, frame.reason ? `[ssh channel closed: ${frame.reason}]` : undefined);
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
- finish(started ? 1 : 1, '[lost the connection to skrr; the SSH channel has ended]');
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
  }
@@ -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 || kind === 'ci')
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 === 'ci')
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
+ }
@@ -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
@@ -94,7 +94,7 @@ function canMutatePersistedCredential(credential, bareMode) {
94
94
  return false;
95
95
  if (bareMode)
96
96
  return false;
97
- if (credential.kind === 'ci')
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` env (CI / static; Phase I adds
17
- * server-side scope=ci validation)
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