@skrr-ai/cli 0.1.79 → 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 (114) hide show
  1. package/dist/base-command.d.ts +4 -0
  2. package/dist/base-command.js +40 -4
  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/inbox/index.js +4 -2
  8. package/dist/commands/list-tokens.d.ts +0 -1
  9. package/dist/commands/list-tokens.js +1 -1
  10. package/dist/commands/login.d.ts +8 -3
  11. package/dist/commands/login.js +14 -4
  12. package/dist/commands/machines/dedicated/ssh-proxy.js +1 -0
  13. package/dist/commands/revoke-token.d.ts +0 -1
  14. package/dist/commands/revoke-token.js +1 -1
  15. package/dist/commands/spaces/workflows/events.js +9 -2
  16. package/dist/commands/store/follow.d.ts +1 -0
  17. package/dist/commands/store/follow.js +7 -1
  18. package/dist/commands/store/follows/list.d.ts +11 -0
  19. package/dist/commands/store/follows/list.js +29 -0
  20. package/dist/commands/store/follows/remove.d.ts +16 -0
  21. package/dist/commands/store/follows/remove.js +37 -0
  22. package/dist/commands/store/publishers/profile/draft.d.ts +17 -0
  23. package/dist/commands/store/publishers/profile/draft.js +54 -0
  24. package/dist/commands/store/publishers/profile/handle.d.ts +14 -0
  25. package/dist/commands/store/publishers/profile/handle.js +39 -0
  26. package/dist/commands/store/publishers/profile/hide.d.ts +12 -0
  27. package/dist/commands/store/publishers/profile/hide.js +39 -0
  28. package/dist/commands/store/publishers/profile/preview.d.ts +11 -0
  29. package/dist/commands/store/publishers/profile/preview.js +39 -0
  30. package/dist/commands/store/publishers/profile/publish.d.ts +12 -0
  31. package/dist/commands/store/publishers/profile/publish.js +41 -0
  32. package/dist/commands/store/publishers/profile/restore.d.ts +15 -0
  33. package/dist/commands/store/publishers/profile/restore.js +49 -0
  34. package/dist/commands/store/publishers/profile/revisions.d.ts +11 -0
  35. package/dist/commands/store/publishers/profile/revisions.js +30 -0
  36. package/dist/commands/store/publishers/profile/show.d.ts +11 -0
  37. package/dist/commands/store/publishers/profile/show.js +39 -0
  38. package/dist/commands/store/review.d.ts +26 -0
  39. package/dist/commands/store/review.js +93 -0
  40. package/dist/commands/store/reviews.d.ts +29 -0
  41. package/dist/commands/store/reviews.js +170 -0
  42. package/dist/commands/store/tip.d.ts +20 -0
  43. package/dist/commands/store/tip.js +57 -0
  44. package/dist/commands/tokens/create.d.ts +28 -0
  45. package/dist/commands/tokens/create.js +136 -0
  46. package/dist/commands/tokens/list.d.ts +11 -0
  47. package/dist/commands/tokens/list.js +53 -0
  48. package/dist/commands/tokens/revoke.d.ts +14 -0
  49. package/dist/commands/tokens/revoke.js +42 -0
  50. package/dist/lib/access-tokens.d.ts +59 -0
  51. package/dist/lib/access-tokens.js +50 -0
  52. package/dist/lib/agentic-stream.js +13 -4
  53. package/dist/lib/api-fetch.js +6 -2
  54. package/dist/lib/auth-core-init.d.ts +6 -0
  55. package/dist/lib/auth-core-init.js +28 -6
  56. package/dist/lib/brokered-dpop.d.ts +34 -0
  57. package/dist/lib/brokered-dpop.js +89 -0
  58. package/dist/lib/credential-resolver.d.ts +1 -1
  59. package/dist/lib/credential-resolver.js +8 -2
  60. package/dist/lib/daemonBroker.d.ts +84 -4
  61. package/dist/lib/daemonBroker.js +209 -40
  62. package/dist/lib/daemonBrokerRefusal.d.ts +26 -0
  63. package/dist/lib/daemonBrokerRefusal.js +83 -4
  64. package/dist/lib/daemonRestartWait.d.ts +39 -0
  65. package/dist/lib/daemonRestartWait.js +181 -0
  66. package/dist/lib/dedicated-service.js +10 -5
  67. package/dist/lib/dedicated-ssh.d.ts +9 -0
  68. package/dist/lib/dedicated-ssh.js +235 -63
  69. package/dist/lib/dedicated-terminal.js +6 -3
  70. package/dist/lib/dpop-auth.d.ts +35 -0
  71. package/dist/lib/dpop-auth.js +57 -0
  72. package/dist/lib/login.d.ts +6 -0
  73. package/dist/lib/login.js +13 -4
  74. package/dist/lib/node-adapter.js +6 -2
  75. package/dist/lib/publisher-profile-flags.d.ts +19 -0
  76. package/dist/lib/publisher-profile-flags.js +48 -0
  77. package/dist/lib/refresh.d.ts +1 -1
  78. package/dist/lib/refresh.js +6 -1
  79. package/dist/lib/ssh-ordered-bytes.d.ts +8 -0
  80. package/dist/lib/ssh-ordered-bytes.js +31 -0
  81. package/dist/lib/usd-micros.d.ts +9 -0
  82. package/dist/lib/usd-micros.js +21 -0
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +42 -1
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +35 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +22 -3
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +29 -5
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +49 -1
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +47 -9
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -1
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +7 -1
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +56 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +42 -20
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.d.ts +101 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +179 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -1
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +23 -5
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +42 -1
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +32 -2
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +22 -3
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +27 -4
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +49 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +47 -9
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -1
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +7 -1
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +56 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +39 -20
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.d.ts +101 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +138 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -1
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -1
  111. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  112. package/dist/node_modules/@skrr-ai/data-provider/index.js +3895 -3882
  113. package/oclif.manifest.json +34985 -33643
  114. package/package.json +28 -4
@@ -0,0 +1,35 @@
1
+ /**
2
+ * dpop-auth.ts — attach RFC 9449 proofs for a DPoP-bound hand-off token
3
+ * (OSK-12020).
4
+ *
5
+ * The ONE place a request path asks for a proof, so every transport the CLI
6
+ * has — `apiFetch`, the data-service node adapter, SSE, and Socket.IO — binds
7
+ * the same way. For any other credential both helpers are no-ops and the wire
8
+ * is byte-identical to before.
9
+ *
10
+ * The CLI never holds the key. Each proof comes from the local daemon
11
+ * (`brokeredDpopProof`), which keeps the private half in its own memory; that
12
+ * is what makes a copied token useless anywhere but on this machine.
13
+ */
14
+ /** RFC 9449 §4.1. */
15
+ export declare const DPOP_HEADER = "DPoP";
16
+ /** Headers to add to one HTTP request made with `token`; `{}` when unbound. */
17
+ export declare function dpopHeadersFor(token: string | null | undefined, method: string, url: string): Promise<Record<string, string>>;
18
+ /**
19
+ * The URI a Socket.IO handshake proof commits to: the realtime origin's
20
+ * http(s) form plus the engine path, no trailing slash — what the server
21
+ * rebuilds from the handshake request (`reconstructHandshakeHtu`).
22
+ */
23
+ export declare function socketHandshakeHtu(wsOrigin: string, ioPath?: string): string;
24
+ type SocketAuth = Record<string, unknown>;
25
+ type SocketAuthFn = (cb: (data: SocketAuth) => void) => void;
26
+ /**
27
+ * Socket.IO `auth` for a handshake made with `auth.token`. Unbound: the object
28
+ * itself, unchanged. Bound: a FUNCTION, which socket.io-client calls on every
29
+ * (re)connect — so each handshake presents a fresh, single-use proof instead
30
+ * of replaying the first one into the server's jti cache.
31
+ */
32
+ export declare function socketAuthWithDpop(auth: SocketAuth & {
33
+ token?: string | null;
34
+ }, wsOrigin: string, ioPath?: string): SocketAuth | SocketAuthFn;
35
+ export {};
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * dpop-auth.ts — attach RFC 9449 proofs for a DPoP-bound hand-off token
4
+ * (OSK-12020).
5
+ *
6
+ * The ONE place a request path asks for a proof, so every transport the CLI
7
+ * has — `apiFetch`, the data-service node adapter, SSE, and Socket.IO — binds
8
+ * the same way. For any other credential both helpers are no-ops and the wire
9
+ * is byte-identical to before.
10
+ *
11
+ * The CLI never holds the key. Each proof comes from the local daemon
12
+ * (`brokeredDpopProof`), which keeps the private half in its own memory; that
13
+ * is what makes a copied token useless anywhere but on this machine.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.DPOP_HEADER = void 0;
17
+ exports.dpopHeadersFor = dpopHeadersFor;
18
+ exports.socketHandshakeHtu = socketHandshakeHtu;
19
+ exports.socketAuthWithDpop = socketAuthWithDpop;
20
+ const brokered_dpop_1 = require("./brokered-dpop");
21
+ /** RFC 9449 §4.1. */
22
+ exports.DPOP_HEADER = 'DPoP';
23
+ /** Headers to add to one HTTP request made with `token`; `{}` when unbound. */
24
+ async function dpopHeadersFor(token, method, url) {
25
+ if (!(0, brokered_dpop_1.isDpopBoundHandoffToken)(token))
26
+ return {};
27
+ const proof = await (0, brokered_dpop_1.brokeredDpopProof)(token, method, url);
28
+ return proof ? { [exports.DPOP_HEADER]: proof } : {};
29
+ }
30
+ /**
31
+ * The URI a Socket.IO handshake proof commits to: the realtime origin's
32
+ * http(s) form plus the engine path, no trailing slash — what the server
33
+ * rebuilds from the handshake request (`reconstructHandshakeHtu`).
34
+ */
35
+ function socketHandshakeHtu(wsOrigin, ioPath = '/ws/agentic') {
36
+ const u = new URL(wsOrigin);
37
+ u.protocol = u.protocol === 'wss:' ? 'https:' : u.protocol === 'ws:' ? 'http:' : u.protocol;
38
+ u.pathname = ioPath.replace(/\/+$/, '');
39
+ u.search = '';
40
+ u.hash = '';
41
+ return u.toString();
42
+ }
43
+ /**
44
+ * Socket.IO `auth` for a handshake made with `auth.token`. Unbound: the object
45
+ * itself, unchanged. Bound: a FUNCTION, which socket.io-client calls on every
46
+ * (re)connect — so each handshake presents a fresh, single-use proof instead
47
+ * of replaying the first one into the server's jti cache.
48
+ */
49
+ function socketAuthWithDpop(auth, wsOrigin, ioPath = '/ws/agentic') {
50
+ const token = typeof auth.token === 'string' ? auth.token : null;
51
+ if (!(0, brokered_dpop_1.isDpopBoundHandoffToken)(token))
52
+ return auth;
53
+ const htu = socketHandshakeHtu(wsOrigin, ioPath);
54
+ return (cb) => {
55
+ void (0, brokered_dpop_1.brokeredDpopProof)(token, 'GET', htu).then((proof) => cb(proof ? { ...auth, dpop: proof } : auth), () => cb(auth));
56
+ };
57
+ }
@@ -42,6 +42,12 @@ export interface CliLoginWithRefreshTokenOptions {
42
42
  * What a shell with no terminal can do instead (OSK-12117). The refusal used to
43
43
  * name only CI tokens, which is not a sign-in: an agent session asking for one
44
44
  * could only hand the job to a person typing `! skrr login`.
45
+ *
46
+ * It also used to send CI to `skrr create-token` + `OVERSKY_TOKEN=osk_ci_...`.
47
+ * That token only bootstraps a daemon; every `skrr` command rejects it with 401
48
+ * (create-token.ts, base-command.ts), so the advice was a dead end (OSK-13209).
49
+ * The route that works today is a minted machine session — see
50
+ * docs/cli/SKRR_AUTH.md "CI today" for its caveats.
45
51
  */
46
52
  export declare const HEADLESS_LOGIN_HINT: string;
47
53
  /**
package/dist/lib/login.js CHANGED
@@ -43,10 +43,18 @@ const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
43
43
  * What a shell with no terminal can do instead (OSK-12117). The refusal used to
44
44
  * name only CI tokens, which is not a sign-in: an agent session asking for one
45
45
  * could only hand the job to a person typing `! skrr login`.
46
+ *
47
+ * It also used to send CI to `skrr create-token` + `OVERSKY_TOKEN=osk_ci_...`.
48
+ * That token only bootstraps a daemon; every `skrr` command rejects it with 401
49
+ * (create-token.ts, base-command.ts), so the advice was a dead end (OSK-13209).
50
+ * The route that works today is a minted machine session — see
51
+ * docs/cli/SKRR_AUTH.md "CI today" for its caveats.
46
52
  */
47
53
  exports.HEADLESS_LOGIN_HINT = '`skrr login --device-code` works without a terminal: it prints a URL and a code for a ' +
48
- 'person to approve in any browser, and finishes when they do. For CI, use ' +
49
- '`skrr create-token` and set OVERSKY_TOKEN=osk_ci_...';
54
+ 'person to approve in any browser, and finishes when they do. With no person to approve ' +
55
+ '(CI, a script), mint a session for this machine elsewhere with `skrr token mint --for <id>` ' +
56
+ 'and run `skrr login` here with OVERSKY_REFRESH_TOKEN and OVERSKY_CLI_ID set. ' +
57
+ '`skrr create-token` tokens are not accepted by skrr commands.';
50
58
  /**
51
59
  * The line printed when the daemon descriptor this CLI found is for another
52
60
  * server (OSK-12117).
@@ -164,8 +172,9 @@ async function cliLogin(opts = {}) {
164
172
  // silently fall through to PKCE / device-code below.
165
173
  if (brokerResult.reason === 'ci_token_refused') {
166
174
  throw new Error('The skrr background service on this computer is bootstrapped from a CI token, which cannot broker an interactive CLI login. ' +
167
- 'Use `skrr create-token --name <name>` to mint a long-lived CI credential directly, ' +
168
- 'or run `skrr login` on a machine where the daemon was authenticated interactively.');
175
+ 'Sign this CLI in on its own with `skrr login --device-code`, or with a session minted elsewhere by ' +
176
+ '`skrr token mint --for <id>` (OVERSKY_REFRESH_TOKEN + OVERSKY_CLI_ID). ' +
177
+ 'A `skrr create-token` token is not accepted by skrr commands.');
169
178
  }
170
179
  // `base_url_mismatch` is a configuration drift the user can resolve
171
180
  // explicitly; print a one-line hint so they're not surprised when
@@ -12,6 +12,7 @@ const config_1 = require("./config");
12
12
  const auth_storage_1 = require("./auth-storage");
13
13
  const credential_resolver_1 = require("./credential-resolver");
14
14
  const daemonBroker_1 = require("./daemonBroker");
15
+ const dpop_auth_1 = require("./dpop-auth");
15
16
  const refresh_1 = require("./refresh");
16
17
  const delegated_cli_1 = require("./delegated-cli");
17
18
  const workspace_header_1 = require("./workspace-header");
@@ -159,6 +160,9 @@ function createNodeAdapter(opts = {}) {
159
160
  }
160
161
  }
161
162
  const finalUrl = appendQuery(resolveUrl(url), options?.params);
163
+ // OSK-12020 — a DPoP-bound hand-off token needs a fresh proof per request
164
+ // (and per retry: proofs are single-use). No-op for every other token.
165
+ Object.assign(headers, await (0, dpop_auth_1.dpopHeadersFor)(currentCredential().token, method, finalUrl));
162
166
  return fetch(finalUrl, { method, headers, body: finalBody });
163
167
  }
164
168
  /**
@@ -198,7 +202,7 @@ function createNodeAdapter(opts = {}) {
198
202
  return false;
199
203
  // Never refresh a CI token — they're static by construction.
200
204
  const { token, kind } = currentCredential();
201
- if (!token || kind === 'ci')
205
+ if (!token || (0, auth_core_1.isStaticTokenKind)(kind))
202
206
  return false;
203
207
  if (refreshInFlight)
204
208
  return refreshInFlight;
@@ -237,7 +241,7 @@ function createNodeAdapter(opts = {}) {
237
241
  if (process.env.OVERSKY_SKIP_DAEMON_BROKER === '1')
238
242
  return false;
239
243
  const cred = currentCredential();
240
- if (cred.kind === 'ci')
244
+ if ((0, auth_core_1.isStaticTokenKind)(cred.kind))
241
245
  return false;
242
246
  // An operator-supplied `--token` is that operator's explicit
243
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
+ }
@@ -22,7 +22,7 @@
22
22
  * - `access_token`: broker mode — a short-lived access token from a
23
23
  * daemon-owned family; nothing durable leaves the daemon.
24
24
  */
25
- export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token"];
25
+ export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token", "access_token_dpop"];
26
26
  export type CliHandoffMode = (typeof CLI_HANDOFF_MODES)[number];
27
27
  /** Broker-mode redeem route on the daemon's loopback server. */
28
28
  export declare const CLI_HANDOFF_TOKEN_PATH = "/v1/auth/cli-handoff-token";
@@ -74,10 +74,26 @@ export declare function normalizeHandoffModes(value: unknown): CliHandoffMode[];
74
74
  */
75
75
  export declare function advertisedHandoffModes(d: CliHandoffDescriptor): CliHandoffMode[];
76
76
  export declare function descriptorOffersAccessToken(d: CliHandoffDescriptor): boolean;
77
+ /** True when the token route can serve DPoP-bound tokens (OSK-12020). */
78
+ export declare function descriptorOffersDpop(d: CliHandoffDescriptor): boolean;
77
79
  export interface CliHandoffTokenRequest {
78
80
  /** Bypass the daemon broker's cached token (the 401-recovery path). */
79
81
  forceRefresh?: boolean;
82
+ /**
83
+ * The caller can fetch per-request proofs from `CLI_HANDOFF_DPOP_PROOF_PATH`
84
+ * and wants a DPoP-bound token. Sent only to a descriptor that advertises
85
+ * `access_token_dpop`; a daemon that does not know the field ignores it and
86
+ * answers an unbound token, which `tokenType` then says.
87
+ */
88
+ dpop?: boolean;
80
89
  }
90
+ /**
91
+ * How the token must be presented. `DPoP` means the token carries `cnf.jkt`
92
+ * and every request needs a fresh proof; absent or `Bearer` means an ordinary
93
+ * bearer token. Read from the RESPONSE, never assumed from the request: an
94
+ * older server ignores the binding request and mints an unbound token.
95
+ */
96
+ export type CliHandoffTokenType = 'Bearer' | 'DPoP';
81
97
  export interface CliHandoffTokenResponse {
82
98
  accessToken: string;
83
99
  /** Absolute expiry in ms since epoch, when the daemon knows it. */
@@ -85,6 +101,31 @@ export interface CliHandoffTokenResponse {
85
101
  /** Server the minted credential belongs to — lets the caller verify it is
86
102
  * about to use a token for the API it thinks it is talking to. */
87
103
  serverUrl?: string;
104
+ tokenType?: CliHandoffTokenType;
88
105
  }
89
106
  /** Parse the redeem response; null when the token itself is absent. */
90
107
  export declare function parseCliHandoffTokenResponse(value: unknown): CliHandoffTokenResponse | null;
108
+ /** The daemon refuses an UNBOUND token because its posture requires binding. */
109
+ export declare const CLI_HANDOFF_ERROR_DPOP_REQUIRED = "HANDOFF_DPOP_REQUIRED";
110
+ /**
111
+ * The daemon signs RFC 9449 proofs for a bound token with a key that never
112
+ * leaves it. Same descriptor secret as the token route: using a stolen bound
113
+ * token therefore needs live loopback access to THIS daemon, which is the
114
+ * boundary the capability already has.
115
+ */
116
+ export declare const CLI_HANDOFF_DPOP_PROOF_PATH = "/v1/auth/cli-handoff-dpop-proof";
117
+ /** The oracle refused to sign (bad htm/htu, or `ath` names no token it issued). */
118
+ export declare const CLI_HANDOFF_ERROR_DPOP_PROOF_REFUSED = "HANDOFF_DPOP_PROOF_REFUSED";
119
+ /** The daemon holds no DPoP key (binding is off on this daemon). */
120
+ export declare const CLI_HANDOFF_ERROR_DPOP_UNAVAILABLE = "HANDOFF_DPOP_UNAVAILABLE";
121
+ export interface CliHandoffDpopProofRequest {
122
+ htm: string;
123
+ htu: string;
124
+ /** base64url SHA-256 of the bound access token. The token itself is never
125
+ * sent: the oracle commits to a hash, and only to one it issued. */
126
+ ath: string;
127
+ }
128
+ export interface CliHandoffDpopProofResponse {
129
+ proof: string;
130
+ }
131
+ export declare function parseCliHandoffDpopProofResponse(value: unknown): CliHandoffDpopProofResponse | null;
@@ -18,19 +18,21 @@
18
18
  * must produce the full shape.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.CLI_HANDOFF_ERROR_BROKER_ONLY = exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = exports.CLI_HANDOFF_TOKEN_PATH = exports.CLI_HANDOFF_MODES = void 0;
21
+ exports.CLI_HANDOFF_ERROR_DPOP_UNAVAILABLE = exports.CLI_HANDOFF_ERROR_DPOP_PROOF_REFUSED = exports.CLI_HANDOFF_DPOP_PROOF_PATH = exports.CLI_HANDOFF_ERROR_DPOP_REQUIRED = exports.CLI_HANDOFF_ERROR_BROKER_ONLY = exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = exports.CLI_HANDOFF_TOKEN_PATH = exports.CLI_HANDOFF_MODES = void 0;
22
22
  exports.parseCliHandoffDescriptor = parseCliHandoffDescriptor;
23
23
  exports.normalizeHandoffModes = normalizeHandoffModes;
24
24
  exports.advertisedHandoffModes = advertisedHandoffModes;
25
25
  exports.descriptorOffersAccessToken = descriptorOffersAccessToken;
26
+ exports.descriptorOffersDpop = descriptorOffersDpop;
26
27
  exports.parseCliHandoffTokenResponse = parseCliHandoffTokenResponse;
28
+ exports.parseCliHandoffDpopProofResponse = parseCliHandoffDpopProofResponse;
27
29
  /**
28
30
  * What a caller may redeem the descriptor secret for.
29
31
  * - `refresh_family`: the legacy durable mint (90-day cli-scope family).
30
32
  * - `access_token`: broker mode — a short-lived access token from a
31
33
  * daemon-owned family; nothing durable leaves the daemon.
32
34
  */
33
- exports.CLI_HANDOFF_MODES = ['refresh_family', 'access_token'];
35
+ exports.CLI_HANDOFF_MODES = ['refresh_family', 'access_token', 'access_token_dpop'];
34
36
  /** Broker-mode redeem route on the daemon's loopback server. */
35
37
  exports.CLI_HANDOFF_TOKEN_PATH = '/v1/auth/cli-handoff-token';
36
38
  /** Error codes this contract emits. String literals so both ends compare
@@ -96,7 +98,12 @@ function advertisedHandoffModes(d) {
96
98
  return declared.length > 0 ? declared : ['refresh_family'];
97
99
  }
98
100
  function descriptorOffersAccessToken(d) {
99
- return advertisedHandoffModes(d).includes('access_token');
101
+ const modes = advertisedHandoffModes(d);
102
+ return modes.includes('access_token') || modes.includes('access_token_dpop');
103
+ }
104
+ /** True when the token route can serve DPoP-bound tokens (OSK-12020). */
105
+ function descriptorOffersDpop(d) {
106
+ return advertisedHandoffModes(d).includes('access_token_dpop');
100
107
  }
101
108
  /** Parse the redeem response; null when the token itself is absent. */
102
109
  function parseCliHandoffTokenResponse(value) {
@@ -109,5 +116,30 @@ function parseCliHandoffTokenResponse(value) {
109
116
  accessToken: v.accessToken,
110
117
  ...(typeof v.accessExpiresAt === 'number' ? { accessExpiresAt: v.accessExpiresAt } : {}),
111
118
  ...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
119
+ ...(v.tokenType === 'DPoP' || v.tokenType === 'Bearer' ? { tokenType: v.tokenType } : {}),
112
120
  };
113
121
  }
122
+ /** The daemon refuses an UNBOUND token because its posture requires binding. */
123
+ exports.CLI_HANDOFF_ERROR_DPOP_REQUIRED = 'HANDOFF_DPOP_REQUIRED';
124
+ // ---------------------------------------------------------------------
125
+ // Proof oracle — POST CLI_HANDOFF_DPOP_PROOF_PATH (OSK-12020)
126
+ // ---------------------------------------------------------------------
127
+ /**
128
+ * The daemon signs RFC 9449 proofs for a bound token with a key that never
129
+ * leaves it. Same descriptor secret as the token route: using a stolen bound
130
+ * token therefore needs live loopback access to THIS daemon, which is the
131
+ * boundary the capability already has.
132
+ */
133
+ exports.CLI_HANDOFF_DPOP_PROOF_PATH = '/v1/auth/cli-handoff-dpop-proof';
134
+ /** The oracle refused to sign (bad htm/htu, or `ath` names no token it issued). */
135
+ exports.CLI_HANDOFF_ERROR_DPOP_PROOF_REFUSED = 'HANDOFF_DPOP_PROOF_REFUSED';
136
+ /** The daemon holds no DPoP key (binding is off on this daemon). */
137
+ exports.CLI_HANDOFF_ERROR_DPOP_UNAVAILABLE = 'HANDOFF_DPOP_UNAVAILABLE';
138
+ function parseCliHandoffDpopProofResponse(value) {
139
+ if (value == null || typeof value !== 'object')
140
+ return null;
141
+ const v = value;
142
+ if (typeof v.proof !== 'string' || v.proof.split('.').length !== 3)
143
+ return null;
144
+ return { proof: v.proof };
145
+ }
@@ -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
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CredentialResolver = exports.AuthHelperUntrustedError = exports.AuthHelperUnavailableError = exports.CI_TOKEN_PREFIX = exports.CREDENTIAL_PRECEDENCE = void 0;
3
+ exports.CredentialResolver = exports.AuthHelperUntrustedError = exports.AuthHelperUnavailableError = exports.PERSONAL_ACCESS_TOKEN_PREFIX = exports.CI_TOKEN_PREFIX = exports.CREDENTIAL_PRECEDENCE = void 0;
4
+ exports.isStaticTokenKind = isStaticTokenKind;
4
5
  exports.classifyTokenKind = classifyTokenKind;
5
6
  exports.__resetCredentialResolverForTest = __resetCredentialResolverForTest;
6
7
  exports.invalidateAuthHelperCache = invalidateAuthHelperCache;
@@ -19,8 +20,11 @@ exports.invalidateAuthHelperCache = invalidateAuthHelperCache;
19
20
  * Precedence (high → low):
20
21
  *
21
22
  * 1. flag — `--token <jwt>` CLI flag (debugging)
22
- * 2. env-token — `OVERSKY_TOKEN` env (CI / static; Phase I adds
23
- * server-side scope=ci validation)
23
+ * 2. env-token — `OVERSKY_TOKEN` (CI / static). The CLI also reads
24
+ * `SKRR_TOKEN` via `envTokenNames`; `OVERSKY_TOKEN` wins
25
+ * when both are set, matching the CLI config overlay. A
26
+ * personal access token (`osk_pat_*`) is the credential
27
+ * CI should put here.
24
28
  * 3. auth-helper — `OVERSKY_AUTH_HELPER` script output (rotating
25
29
  * creds — Vault, AWS SM, internal SSO proxy)
26
30
  * 4. fd-handoff — `OVERSKY_AUTH_FD` parent→child IPC (Electron
@@ -68,6 +72,16 @@ exports.CREDENTIAL_PRECEDENCE = [
68
72
  ];
69
73
  /** CI-token plaintext prefix; mirrors api/server/services/Auth/daemonCiTokens.js. */
70
74
  exports.CI_TOKEN_PREFIX = 'osk_ci_';
75
+ /** Personal-access-token prefix; mirrors api/server/services/Auth/personalAccessTokens.js. */
76
+ exports.PERSONAL_ACCESS_TOKEN_PREFIX = 'osk_pat_';
77
+ /**
78
+ * True for a kind whose token is static by construction: never refreshed,
79
+ * never rotated into a store. Like `TokenKind` itself this selects a local
80
+ * code path and grants nothing.
81
+ */
82
+ function isStaticTokenKind(kind) {
83
+ return kind === 'ci' || kind === 'pat';
84
+ }
71
85
  /**
72
86
  * Classify a token by local shape. Pure function; safe to call on any string
73
87
  * (including `null`-coerced empty). Returns `'standard'` for empty input
@@ -103,6 +117,8 @@ function classifyTokenKind(token) {
103
117
  return 'standard';
104
118
  if (token.startsWith(exports.CI_TOKEN_PREFIX))
105
119
  return 'ci';
120
+ if (token.startsWith(exports.PERSONAL_ACCESS_TOKEN_PREFIX))
121
+ return 'pat';
106
122
  const payload = (0, jwtUtils_js_1.decodeJwtPayload)(token);
107
123
  if (payload &&
108
124
  typeof payload === 'object' &&
@@ -260,8 +276,16 @@ class CredentialResolver {
260
276
  if (typeof this.opts.envTokenOverride === 'string') {
261
277
  return this.opts.envTokenOverride.length > 0 ? this.opts.envTokenOverride : null;
262
278
  }
263
- const v = process.env.OVERSKY_TOKEN;
264
- return typeof v === 'string' && v.length > 0 ? v : null;
279
+ // `OVERSKY_TOKEN` first (retained by decision, docs/product/skrr-env-inventory.md),
280
+ // then any renamed spelling the caller opted into (`SKRR_TOKEN` for the CLI,
281
+ // OSK-13208). The daemon does not opt in: a CI job that sets `SKRR_TOKEN`
282
+ // for `skrr` must not have a co-located `skrrd` silently adopt it.
283
+ for (const name of this.opts.envTokenNames ?? ['OVERSKY_TOKEN']) {
284
+ const v = process.env[name];
285
+ if (typeof v === 'string' && v.length > 0)
286
+ return v;
287
+ }
288
+ return null;
265
289
  }
266
290
  async tryResolveAuthHelper() {
267
291
  let helperPath;