@skrr-ai/cli 0.1.50 → 0.1.52
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 +7 -0
- package/dist/base-command.js +95 -8
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +8 -4
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +32 -0
- package/dist/commands/agents/update.js +81 -9
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +15 -1
- package/dist/commands/commitments/next-tick.js +12 -2
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +53 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/doctor.js +8 -1
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +26 -7
- package/dist/commands/logout.js +3 -0
- package/dist/commands/machines/dedicated/show.js +25 -2
- package/dist/commands/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +6 -0
- package/dist/commands/tasks/create.js +31 -6
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +29 -1
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +14 -10
- package/dist/lib/agentic-stream.d.ts +88 -1
- package/dist/lib/agentic-stream.js +330 -9
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +68 -0
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +342 -11
- package/dist/lib/cli-id.d.ts +22 -0
- package/dist/lib/cli-id.js +56 -0
- package/dist/lib/cli-identity.d.ts +49 -0
- package/dist/lib/cli-identity.js +159 -0
- package/dist/lib/commitments.d.ts +25 -1
- package/dist/lib/commitments.js +46 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +15 -3
- package/dist/lib/daemonBroker.d.ts +37 -1
- package/dist/lib/daemonBroker.js +146 -22
- package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
- package/dist/lib/daemonBrokerRefusal.js +127 -0
- package/dist/lib/dedicated-machines.d.ts +33 -2
- package/dist/lib/dedicated-machines.js +50 -1
- package/dist/lib/delegated-cli.js +20 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.js +10 -5
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/keychain.d.ts +39 -0
- package/dist/lib/keychain.js +228 -39
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +68 -1
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +67 -21
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/oauthLogin.js +2 -8
- package/dist/lib/refresh.js +6 -2
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +87 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
- package/oclif.manifest.json +28591 -28438
- package/package.json +1 -1
package/dist/lib/auth-storage.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.backends = void 0;
|
|
|
37
37
|
exports.readFromFile = readFromFile;
|
|
38
38
|
exports.deleteFileBackend = deleteFileBackend;
|
|
39
39
|
exports.readFromBackend = readFromBackend;
|
|
40
|
+
exports.describeForeignCredential = describeForeignCredential;
|
|
40
41
|
exports.writeToBackend = writeToBackend;
|
|
41
42
|
exports.clearAllBackends = clearAllBackends;
|
|
42
43
|
exports.normalizeServerOrigin = normalizeServerOrigin;
|
|
@@ -71,6 +72,21 @@ exports.__authFilePathForTest = __authFilePathForTest;
|
|
|
71
72
|
* interactive) and delegates the keychain/file slots to `readFromBackend()`
|
|
72
73
|
* here. We own token persistence; the resolver owns precedence.
|
|
73
74
|
*
|
|
75
|
+
* Every bundle is also bound to the CONFIG that owns it (OSK-12135). The
|
|
76
|
+
* login Keychain is shared by every skrr setup of this macOS user, while the
|
|
77
|
+
* CLI identity (`cliId`) lives in the config root, so another setup with the
|
|
78
|
+
* same profile name could write this profile's slot and the next refresh
|
|
79
|
+
* presented a token bound to its `cliId`. Now:
|
|
80
|
+
*
|
|
81
|
+
* - a bundle carries its owner `{configRoot, cliId}` (Keychain `cli-owner`
|
|
82
|
+
* slot, or `owner` in the file), and the server-bound identity is also read
|
|
83
|
+
* from the access token's `did` claim, which an older writer cannot omit;
|
|
84
|
+
* - a read REFUSES a bundle whose owner is another root or another cliId,
|
|
85
|
+
* with the owner named (`ownerStatus: 'foreign'`), and never deletes it;
|
|
86
|
+
* - storage is namespaced by config root (see `keychain.ts`; the file lives
|
|
87
|
+
* under the config root), and a slot another root may own is only ever a
|
|
88
|
+
* read-side migration source, adopted on POSITIVE evidence of ownership.
|
|
89
|
+
*
|
|
74
90
|
* Cross-process coordination: every write path should run inside
|
|
75
91
|
* `withAuthLock()` from @skrr-ai/auth-core so the daemon, Electron main,
|
|
76
92
|
* and a sibling `skrr` can't race us on a rotation. Callers are responsible
|
|
@@ -84,17 +100,127 @@ const auth_core_1 = require("@skrr-ai/auth-core");
|
|
|
84
100
|
const keychain = __importStar(require("./keychain"));
|
|
85
101
|
const cred_envelope_1 = require("./cred-envelope");
|
|
86
102
|
const config_1 = require("./config");
|
|
103
|
+
const cli_id_1 = require("./cli-id");
|
|
87
104
|
/** Filename for the non-darwin file backend. Parallel to the daemon's
|
|
88
105
|
* `auth.json`, deliberately distinct so the two binaries never clobber
|
|
89
106
|
* each other's tokens (CLI holds scope=cli, daemon holds scope=daemon). */
|
|
90
107
|
const AUTH_FILENAME = 'cli-auth.json';
|
|
91
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* The config root — the same one `cli-config.json` (and so `cliId`) lives in.
|
|
110
|
+
*
|
|
111
|
+
* This was `$HOME/.skrr` regardless of `SKRR_CONFIG_DIR`, so two configs that
|
|
112
|
+
* shared a HOME shared one credential file while each held its own `cliId`.
|
|
113
|
+
* Unchanged for anyone who has not relocated the root.
|
|
114
|
+
*/
|
|
92
115
|
function authDir() {
|
|
93
|
-
return
|
|
116
|
+
return (0, config_1.configRoot)();
|
|
94
117
|
}
|
|
95
118
|
function authFilePath() {
|
|
96
119
|
return path.join(authDir(), AUTH_FILENAME);
|
|
97
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Where the file lived before it followed the config root. Only differs from
|
|
123
|
+
* `authFilePath()` under a root override, and is then a migration SOURCE: read
|
|
124
|
+
* only when its bundle positively belongs to this config, never written.
|
|
125
|
+
*/
|
|
126
|
+
function legacyAuthFilePath() {
|
|
127
|
+
return path.join(os.homedir(), '.skrr', AUTH_FILENAME);
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------
|
|
130
|
+
// Ownership
|
|
131
|
+
// ---------------------------------------------------------------------
|
|
132
|
+
/** Who this process is: its resolved config root and the cliId it presents. */
|
|
133
|
+
function currentOwner() {
|
|
134
|
+
let cliId;
|
|
135
|
+
try {
|
|
136
|
+
// The id this profile PRESENTS — its own when it was issued one (OSK-12156).
|
|
137
|
+
const value = (0, cli_id_1.resolveCliId)((0, config_1.loadConfig)());
|
|
138
|
+
if (value)
|
|
139
|
+
cliId = value;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A corrupt config cannot tell us who we are; ownership then rests on the
|
|
143
|
+
// config root alone, which is still enough to refuse another root's bundle.
|
|
144
|
+
}
|
|
145
|
+
return { configRoot: path.resolve((0, config_1.configRoot)()), ...(cliId ? { cliId } : {}) };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The CLI identity the SERVER bound this credential to. CLI access tokens carry
|
|
149
|
+
* it as `did` (the refresh row's `daemonId`), so this answers for bundles written
|
|
150
|
+
* by any version — including an older binary that never stored an owner record.
|
|
151
|
+
*
|
|
152
|
+
* Unverified decode, which `jwtUtils` permits for routing and not for trust.
|
|
153
|
+
* This is routing: it picks which LOCAL slot this config may present from. The
|
|
154
|
+
* server still verifies the token and refuses a rotation against another cliId;
|
|
155
|
+
* reading the claim first only stops us presenting a credential that cannot be
|
|
156
|
+
* ours, and anyone able to forge the slot's contents can already write them.
|
|
157
|
+
*/
|
|
158
|
+
function boundCliId(token) {
|
|
159
|
+
const payload = (0, auth_core_1.decodeJwtPayload)(token);
|
|
160
|
+
return payload && typeof payload.did === 'string' && payload.did.length > 0
|
|
161
|
+
? payload.did
|
|
162
|
+
: undefined;
|
|
163
|
+
}
|
|
164
|
+
function parseOwner(raw) {
|
|
165
|
+
let value = raw;
|
|
166
|
+
if (typeof value === 'string') {
|
|
167
|
+
try {
|
|
168
|
+
value = JSON.parse(value);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!value || typeof value !== 'object')
|
|
175
|
+
return undefined;
|
|
176
|
+
const { configRoot: root, cliId } = value;
|
|
177
|
+
const owner = {};
|
|
178
|
+
if (typeof root === 'string' && root.length > 0)
|
|
179
|
+
owner.configRoot = root;
|
|
180
|
+
if (typeof cliId === 'string' && cliId.length > 0)
|
|
181
|
+
owner.cliId = cliId;
|
|
182
|
+
return owner.configRoot || owner.cliId ? owner : undefined;
|
|
183
|
+
}
|
|
184
|
+
function ownerFor(bundle) {
|
|
185
|
+
const me = currentOwner();
|
|
186
|
+
const cliId = boundCliId(bundle.token) ?? me.cliId;
|
|
187
|
+
return { configRoot: me.configRoot, ...(cliId ? { cliId } : {}) };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Does this bundle belong to this config?
|
|
191
|
+
*
|
|
192
|
+
* `ownSlot` is whether the bundle came from the slot this root owns. There,
|
|
193
|
+
* the bundle is ours unless there is evidence it is not. From a slot another
|
|
194
|
+
* root may own (a migration source), it is ours only on POSITIVE evidence —
|
|
195
|
+
* a matching cliId or root — or, per the legacy rule, when it names nobody and
|
|
196
|
+
* this is the account's default root.
|
|
197
|
+
*
|
|
198
|
+
* The token's `did` outranks the stored owner record: an older binary writes
|
|
199
|
+
* the token but not `cli-owner`, so a record that disagrees with the token it
|
|
200
|
+
* sits beside is stale and says nothing about the token.
|
|
201
|
+
*/
|
|
202
|
+
function judgeOwnership(bundle, me, ownSlot) {
|
|
203
|
+
const did = boundCliId(bundle.token);
|
|
204
|
+
const record = bundle.owner;
|
|
205
|
+
const recordValid = record && (!did || !record.cliId || record.cliId === did);
|
|
206
|
+
const storedRoot = recordValid && record.configRoot ? path.resolve(record.configRoot) : undefined;
|
|
207
|
+
const storedCliId = did ?? (recordValid ? record.cliId : undefined);
|
|
208
|
+
const stored = {
|
|
209
|
+
...(storedRoot ? { configRoot: storedRoot } : {}),
|
|
210
|
+
...(storedCliId ? { cliId: storedCliId } : {}),
|
|
211
|
+
};
|
|
212
|
+
if (storedRoot && storedRoot !== me.configRoot)
|
|
213
|
+
return { ours: false, stored };
|
|
214
|
+
if (storedCliId && me.cliId && storedCliId !== me.cliId)
|
|
215
|
+
return { ours: false, stored };
|
|
216
|
+
if (ownSlot)
|
|
217
|
+
return { ours: true, stored };
|
|
218
|
+
const positive = (storedCliId !== undefined && storedCliId === me.cliId) || storedRoot === me.configRoot;
|
|
219
|
+
if (positive)
|
|
220
|
+
return { ours: true, stored };
|
|
221
|
+
const namesNobody = !storedCliId && !storedRoot;
|
|
222
|
+
return { ours: namesNobody && keychain.isDefaultConfigRoot(me.configRoot), stored };
|
|
223
|
+
}
|
|
98
224
|
// ---------------------------------------------------------------------
|
|
99
225
|
// File backend
|
|
100
226
|
// ---------------------------------------------------------------------
|
|
@@ -106,7 +232,40 @@ function authFilePath() {
|
|
|
106
232
|
* retiring it. Everything else should keep using `readFromBackend()`.
|
|
107
233
|
*/
|
|
108
234
|
function readFromFile() {
|
|
109
|
-
|
|
235
|
+
return readFileAssessed().bundle;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* This root's file, refused when it belongs to another config; else, under a
|
|
239
|
+
* root override, the pre-override `$HOME/.skrr` file when it is positively ours.
|
|
240
|
+
*/
|
|
241
|
+
function readFileAssessed() {
|
|
242
|
+
const me = currentOwner();
|
|
243
|
+
const own = readFileAt(authFilePath(), true);
|
|
244
|
+
if (own) {
|
|
245
|
+
const judged = judgeOwnership(own, me, true);
|
|
246
|
+
return judged.ours ? { bundle: own } : { bundle: null, foreign: judged.stored };
|
|
247
|
+
}
|
|
248
|
+
const legacy = legacyAuthFilePath();
|
|
249
|
+
if (path.resolve(legacy) === path.resolve(authFilePath()))
|
|
250
|
+
return { bundle: null };
|
|
251
|
+
const adopted = readFileAt(legacy, false);
|
|
252
|
+
if (adopted && judgeOwnership(adopted, me, false).ours)
|
|
253
|
+
return { bundle: adopted };
|
|
254
|
+
return { bundle: null };
|
|
255
|
+
}
|
|
256
|
+
/** Is the pre-override file present AND positively this config's? */
|
|
257
|
+
function legacyFileIsOurs() {
|
|
258
|
+
const legacy = legacyAuthFilePath();
|
|
259
|
+
if (path.resolve(legacy) === path.resolve(authFilePath()))
|
|
260
|
+
return false;
|
|
261
|
+
const bundle = readFileAt(legacy, false);
|
|
262
|
+
return Boolean(bundle && judgeOwnership(bundle, currentOwner(), false).ours);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Parse one credential file. `upgradeInPlace` lets the envelope migration
|
|
266
|
+
* rewrite it — true only for this root's own file, never a migration source.
|
|
267
|
+
*/
|
|
268
|
+
function readFileAt(p, upgradeInPlace) {
|
|
110
269
|
if (!fs.existsSync(p))
|
|
111
270
|
return null;
|
|
112
271
|
let raw;
|
|
@@ -157,6 +316,9 @@ function readFromFile() {
|
|
|
157
316
|
if (typeof parsed.serverOrigin === 'string' && parsed.serverOrigin.length > 0) {
|
|
158
317
|
bundle.serverOrigin = parsed.serverOrigin;
|
|
159
318
|
}
|
|
319
|
+
const owner = parseOwner(parsed.owner);
|
|
320
|
+
if (owner)
|
|
321
|
+
bundle.owner = owner;
|
|
160
322
|
// Best-effort upgrade: re-write the bundle so legacy plaintext fields
|
|
161
323
|
// become wrapped on the next read. A failure here doesn't break the
|
|
162
324
|
// read path — the caller already has the plaintext bundle.
|
|
@@ -186,7 +348,7 @@ function readFromFile() {
|
|
|
186
348
|
// in-process "lock held" guard (AsyncLocalStorage) — but atomic
|
|
187
349
|
// rename already prevents corruption, so the escalation target is
|
|
188
350
|
// IO efficiency only.
|
|
189
|
-
if (needsMigration) {
|
|
351
|
+
if (needsMigration && upgradeInPlace) {
|
|
190
352
|
try {
|
|
191
353
|
writeToFile(bundle);
|
|
192
354
|
}
|
|
@@ -218,6 +380,9 @@ function writeToFile(bundle) {
|
|
|
218
380
|
if (bundle.serverOrigin) {
|
|
219
381
|
wireBundle.serverOrigin = bundle.serverOrigin;
|
|
220
382
|
}
|
|
383
|
+
// Rewrites of an existing bundle (the envelope upgrade) keep its recorded
|
|
384
|
+
// owner; everything else is stamped with this config.
|
|
385
|
+
wireBundle.owner = bundle.owner ?? ownerFor(bundle);
|
|
221
386
|
// tmp + rename so no racing reader ever sees a partial file; chmod 0600
|
|
222
387
|
// on the final file so the same-UID blast-radius is as narrow as the
|
|
223
388
|
// file backend can offer.
|
|
@@ -251,6 +416,17 @@ function writeToFile(bundle) {
|
|
|
251
416
|
*/
|
|
252
417
|
function deleteFileBackend() {
|
|
253
418
|
const p = authFilePath();
|
|
419
|
+
// This root's own file — unless it holds another config's credential, which
|
|
420
|
+
// is not ours to destroy (an older binary of another config can write here).
|
|
421
|
+
const own = readFileAt(p, false);
|
|
422
|
+
if (!own || judgeOwnership(own, currentOwner(), true).ours)
|
|
423
|
+
unlinkQuietly(p);
|
|
424
|
+
// The pre-override migration source, only when it is positively ours:
|
|
425
|
+
// otherwise a sign-out would leave it to be adopted again on the next read.
|
|
426
|
+
if (legacyFileIsOurs())
|
|
427
|
+
unlinkQuietly(legacyAuthFilePath());
|
|
428
|
+
}
|
|
429
|
+
function unlinkQuietly(p) {
|
|
254
430
|
try {
|
|
255
431
|
if (fs.existsSync(p))
|
|
256
432
|
fs.unlinkSync(p);
|
|
@@ -285,11 +461,95 @@ function readFromKeychain() {
|
|
|
285
461
|
const serverOrigin = keychain.readServerOrigin();
|
|
286
462
|
if (serverOrigin)
|
|
287
463
|
bundle.serverOrigin = serverOrigin;
|
|
464
|
+
const owner = parseOwner(keychain.readOwner());
|
|
465
|
+
if (owner)
|
|
466
|
+
bundle.owner = owner;
|
|
467
|
+
return bundle;
|
|
468
|
+
}
|
|
469
|
+
function bundleFromSlots(slots) {
|
|
470
|
+
const bundle = { token: slots.token.token };
|
|
471
|
+
if (slots.token.expiresAt !== undefined)
|
|
472
|
+
bundle.expiresAt = slots.token.expiresAt;
|
|
473
|
+
if (slots.refresh) {
|
|
474
|
+
bundle.refreshToken = slots.refresh.token;
|
|
475
|
+
if (slots.refresh.expiresAt !== undefined)
|
|
476
|
+
bundle.refreshExpiresAt = slots.refresh.expiresAt;
|
|
477
|
+
}
|
|
478
|
+
if (slots.serverOrigin)
|
|
479
|
+
bundle.serverOrigin = slots.serverOrigin;
|
|
480
|
+
const owner = parseOwner(slots.owner);
|
|
481
|
+
if (owner)
|
|
482
|
+
bundle.owner = owner;
|
|
288
483
|
return bundle;
|
|
289
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Slots this root may adopt from, in order: the legacy profile-only slot, then
|
|
487
|
+
* the slot the default root displaced this config's credential into.
|
|
488
|
+
*/
|
|
489
|
+
function migrationSlots(me) {
|
|
490
|
+
if (keychain.isLegacySlotCurrent())
|
|
491
|
+
return [];
|
|
492
|
+
return [
|
|
493
|
+
{ kind: 'legacy' },
|
|
494
|
+
...(me.cliId ? [{ kind: 'displaced', cliId: me.cliId }] : []),
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* This root's own slot, refused when it belongs to another config; else a
|
|
499
|
+
* migration slot that positively belongs to this config.
|
|
500
|
+
*
|
|
501
|
+
* The migration is READ-ONLY, deliberately. Copying the bundle into our slot
|
|
502
|
+
* here would be a write outside the auth lock, and a copy that lands after a
|
|
503
|
+
* concurrent rotation puts a spent refresh token back — which the server treats
|
|
504
|
+
* as theft and answers by revoking the family. The next locked write (a refresh
|
|
505
|
+
* or a login) lands in our own slot, and from then on it wins.
|
|
506
|
+
*/
|
|
507
|
+
function readKeychainAssessed() {
|
|
508
|
+
if (!keychain.isAvailable())
|
|
509
|
+
return { bundle: null };
|
|
510
|
+
const me = currentOwner();
|
|
511
|
+
const own = readFromKeychain();
|
|
512
|
+
if (own) {
|
|
513
|
+
const judged = judgeOwnership(own, me, true);
|
|
514
|
+
return judged.ours ? { bundle: own } : { bundle: null, foreign: judged.stored };
|
|
515
|
+
}
|
|
516
|
+
for (const slot of migrationSlots(me)) {
|
|
517
|
+
const slots = keychain.readBundleAt(slot);
|
|
518
|
+
if (!slots)
|
|
519
|
+
continue;
|
|
520
|
+
const bundle = bundleFromSlots(slots);
|
|
521
|
+
if (judgeOwnership(bundle, me, false).ours)
|
|
522
|
+
return { bundle };
|
|
523
|
+
}
|
|
524
|
+
return { bundle: null };
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Before the default root overwrites its own slot, move a credential another
|
|
528
|
+
* config's older binary wrote there to that config's displaced slot, keyed by
|
|
529
|
+
* the cliId the server bound it to. A login here must not destroy it: once
|
|
530
|
+
* that config runs a current binary it adopts it from there.
|
|
531
|
+
*/
|
|
532
|
+
function rescueForeignCredential(me) {
|
|
533
|
+
if (!keychain.isLegacySlotCurrent())
|
|
534
|
+
return;
|
|
535
|
+
const token = keychain.readToken();
|
|
536
|
+
if (!token)
|
|
537
|
+
return;
|
|
538
|
+
const quick = boundCliId(token.token);
|
|
539
|
+
if (quick && quick === me.cliId)
|
|
540
|
+
return; // the common case: it is ours
|
|
541
|
+
const slots = keychain.readBundleAt({ kind: 'legacy' });
|
|
542
|
+
if (!slots)
|
|
543
|
+
return;
|
|
544
|
+
const judged = judgeOwnership(bundleFromSlots(slots), me, true);
|
|
545
|
+
if (judged.ours || !judged.stored.cliId)
|
|
546
|
+
return;
|
|
547
|
+
keychain.copyBundle({ kind: 'legacy' }, { kind: 'displaced', cliId: judged.stored.cliId });
|
|
548
|
+
}
|
|
290
549
|
function writeToKeychain(bundle) {
|
|
291
550
|
if (!keychain.isAvailable())
|
|
292
551
|
return false;
|
|
552
|
+
rescueForeignCredential(currentOwner());
|
|
293
553
|
const ok = keychain.writeToken(bundle.token, bundle.expiresAt);
|
|
294
554
|
if (!ok)
|
|
295
555
|
return false;
|
|
@@ -311,11 +571,34 @@ function writeToKeychain(bundle) {
|
|
|
311
571
|
// token pair. Origin-aware readers will fail closed until the next login.
|
|
312
572
|
keychain.deleteServerOrigin();
|
|
313
573
|
}
|
|
574
|
+
// Written last, so the token it describes is already in place.
|
|
575
|
+
if (!keychain.writeOwner(JSON.stringify(bundle.owner ?? ownerFor(bundle))))
|
|
576
|
+
return false;
|
|
314
577
|
return true;
|
|
315
578
|
}
|
|
579
|
+
/** This root's own slot, after a failed partial write. */
|
|
316
580
|
function deleteKeychainBackend() {
|
|
317
581
|
keychain.deleteAll();
|
|
318
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Sign-out for the Keychain: this root's own slot unless it holds another
|
|
585
|
+
* config's credential, plus any migration slot that is positively ours —
|
|
586
|
+
* otherwise a sign-out would leave a bundle the next read adopts again.
|
|
587
|
+
*/
|
|
588
|
+
function clearKeychainBackend() {
|
|
589
|
+
if (!keychain.isAvailable())
|
|
590
|
+
return;
|
|
591
|
+
const me = currentOwner();
|
|
592
|
+
const own = readFromKeychain();
|
|
593
|
+
if (!own || judgeOwnership(own, me, true).ours)
|
|
594
|
+
keychain.deleteAll();
|
|
595
|
+
for (const slot of migrationSlots(me)) {
|
|
596
|
+
const slots = keychain.readBundleAt(slot);
|
|
597
|
+
if (slots && judgeOwnership(bundleFromSlots(slots), me, false).ours) {
|
|
598
|
+
keychain.deleteAllAt(slot);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
319
602
|
// ---------------------------------------------------------------------
|
|
320
603
|
// Public API — backend-selection logic lives here
|
|
321
604
|
// ---------------------------------------------------------------------
|
|
@@ -331,14 +614,21 @@ function deleteKeychainBackend() {
|
|
|
331
614
|
* function returns whatever is persisted.
|
|
332
615
|
*/
|
|
333
616
|
function readFromBackend(expectedServerOrigin) {
|
|
334
|
-
const fromKeychain =
|
|
335
|
-
const keychainResult = assessBundleOrigin(fromKeychain, 'keychain', expectedServerOrigin);
|
|
617
|
+
const fromKeychain = readKeychainAssessed();
|
|
618
|
+
const keychainResult = assessBundleOrigin(fromKeychain.bundle, 'keychain', expectedServerOrigin);
|
|
336
619
|
if (keychainResult.bundle)
|
|
337
620
|
return keychainResult;
|
|
338
|
-
const fromFile =
|
|
339
|
-
const fileResult = assessBundleOrigin(fromFile, 'file', expectedServerOrigin);
|
|
621
|
+
const fromFile = readFileAssessed();
|
|
622
|
+
const fileResult = assessBundleOrigin(fromFile.bundle, 'file', expectedServerOrigin);
|
|
340
623
|
if (fileResult.bundle)
|
|
341
624
|
return fileResult;
|
|
625
|
+
// A bundle that belongs to another skrr config outranks every other
|
|
626
|
+
// diagnostic: it is why this config has no usable credential, and it is the
|
|
627
|
+
// one case where "run login" would otherwise look like it came from nowhere.
|
|
628
|
+
if (fromKeychain.foreign)
|
|
629
|
+
return foreignResult('keychain', fromKeychain.foreign);
|
|
630
|
+
if (fromFile.foreign)
|
|
631
|
+
return foreignResult('file', fromFile.foreign);
|
|
342
632
|
// Prefer the first concrete rejection for diagnostics. A partially-failed
|
|
343
633
|
// Keychain write may leave an unbound token while a valid file fallback is
|
|
344
634
|
// absent; surfacing the status lets BaseCommand explain why it ignored it.
|
|
@@ -359,6 +649,43 @@ function readFromBackend(expectedServerOrigin) {
|
|
|
359
649
|
: {}),
|
|
360
650
|
};
|
|
361
651
|
}
|
|
652
|
+
function foreignResult(backend, storedOwner) {
|
|
653
|
+
const me = currentOwner();
|
|
654
|
+
return {
|
|
655
|
+
bundle: null,
|
|
656
|
+
backend,
|
|
657
|
+
originStatus: 'none',
|
|
658
|
+
ownerStatus: 'foreign',
|
|
659
|
+
storedOwner,
|
|
660
|
+
expectedOwner: me,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* One sentence naming whose credential was refused and why, for the surfaces
|
|
665
|
+
* that explain an absent sign-in (BaseCommand, `skrr doctor`).
|
|
666
|
+
*/
|
|
667
|
+
function describeForeignCredential(result) {
|
|
668
|
+
if (result.ownerStatus !== 'foreign')
|
|
669
|
+
return null;
|
|
670
|
+
const stored = result.storedOwner ?? {};
|
|
671
|
+
const mine = result.expectedOwner ?? {};
|
|
672
|
+
const where = result.backend === 'file' ? 'credential file' : 'Keychain slot';
|
|
673
|
+
const theirs = [
|
|
674
|
+
stored.cliId ? `CLI identity ${stored.cliId}` : null,
|
|
675
|
+
stored.configRoot ? `config root ${stored.configRoot}` : null,
|
|
676
|
+
]
|
|
677
|
+
.filter(Boolean)
|
|
678
|
+
.join(', ');
|
|
679
|
+
const ours = [
|
|
680
|
+
mine.cliId ? `CLI identity ${mine.cliId}` : null,
|
|
681
|
+
mine.configRoot ? `config root ${mine.configRoot}` : null,
|
|
682
|
+
]
|
|
683
|
+
.filter(Boolean)
|
|
684
|
+
.join(', ');
|
|
685
|
+
return (`The credential in this profile's ${where} belongs to another skrr configuration on this ` +
|
|
686
|
+
`machine (${theirs || 'unknown owner'}), not this one (${ours || 'unknown'}). It was not ` +
|
|
687
|
+
`used and has not been deleted.`);
|
|
688
|
+
}
|
|
362
689
|
/**
|
|
363
690
|
* Persist a token bundle to the preferred backend.
|
|
364
691
|
*
|
|
@@ -412,6 +739,9 @@ function writeToBackend(bundle, options = {}) {
|
|
|
412
739
|
}
|
|
413
740
|
persistedBundle = { ...bundle, serverOrigin: normalized };
|
|
414
741
|
}
|
|
742
|
+
// Always stamp THIS config as the owner — a bundle read back and re-written
|
|
743
|
+
// (e.g. `pair`) must not carry a record forward from wherever it came from.
|
|
744
|
+
persistedBundle = { ...persistedBundle, owner: ownerFor(persistedBundle) };
|
|
415
745
|
let backend;
|
|
416
746
|
if (keychain.isAvailable()) {
|
|
417
747
|
if (writeToKeychain(persistedBundle)) {
|
|
@@ -444,7 +774,7 @@ function writeToBackend(bundle, options = {}) {
|
|
|
444
774
|
* auth-failure cleanup). Idempotent.
|
|
445
775
|
*/
|
|
446
776
|
function clearAllBackends() {
|
|
447
|
-
|
|
777
|
+
clearKeychainBackend();
|
|
448
778
|
deleteFileBackend();
|
|
449
779
|
}
|
|
450
780
|
/**
|
|
@@ -468,11 +798,12 @@ function clearAllBackends() {
|
|
|
468
798
|
*/
|
|
469
799
|
exports.backends = {
|
|
470
800
|
readKeychainToken(expectedServerOrigin) {
|
|
471
|
-
const
|
|
801
|
+
const read = readKeychainAssessed();
|
|
802
|
+
const result = assessBundleOrigin(read.bundle, 'keychain', expectedServerOrigin);
|
|
472
803
|
return result.bundle?.token ?? null;
|
|
473
804
|
},
|
|
474
805
|
readFileToken(expectedServerOrigin) {
|
|
475
|
-
const result = assessBundleOrigin(
|
|
806
|
+
const result = assessBundleOrigin(readFileAssessed().bundle, 'file', expectedServerOrigin);
|
|
476
807
|
return result.bundle?.token ?? null;
|
|
477
808
|
},
|
|
478
809
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operator's `OVERSKY_CLI_ID` / `SKRR_CLI_ID`, when set. The legacy spelling
|
|
3
|
+
* wins when both are set, as it does for every other overlay variable
|
|
4
|
+
* (`config.ts`, `envOverlayValue`); the config overlay reads THIS function.
|
|
5
|
+
*/
|
|
6
|
+
export declare function envCliIdOverride(env?: NodeJS.ProcessEnv): string | undefined;
|
|
7
|
+
/** True when this process runs a named profile rather than the default one. */
|
|
8
|
+
export declare function isNamedProfile(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Precedence:
|
|
11
|
+
*
|
|
12
|
+
* 1. `OVERSKY_CLI_ID` / `SKRR_CLI_ID` — the operator said so;
|
|
13
|
+
* 2. `profileCliId` — this named profile was issued its own identity;
|
|
14
|
+
* 3. `cliId` — the machine identity, which the default profile and every
|
|
15
|
+
* profile whose session predates profile identities keep presenting.
|
|
16
|
+
*
|
|
17
|
+
* Never mints: a read must not change who this config is.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveCliId(cfg: {
|
|
20
|
+
cliId?: string;
|
|
21
|
+
profileCliId?: string;
|
|
22
|
+
}): string | undefined;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.envCliIdOverride = envCliIdOverride;
|
|
4
|
+
exports.isNamedProfile = isNamedProfile;
|
|
5
|
+
exports.resolveCliId = resolveCliId;
|
|
6
|
+
/**
|
|
7
|
+
* cli-id.ts — THE cliId this process presents (OSK-12156).
|
|
8
|
+
*
|
|
9
|
+
* The one resolver for the identity presented to the server at login, at
|
|
10
|
+
* refresh, to the daemon broker, and as the owner a stored credential is judged
|
|
11
|
+
* against (`auth-storage.ts`). It used to be read as `cfg.cliId` at a dozen call
|
|
12
|
+
* sites and regenerated in three, which is what made it impossible to give one
|
|
13
|
+
* profile its own id without some reader still presenting the machine one.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately depends on nothing but the active profile: it is read by
|
|
16
|
+
* `config.ts` and `auth-storage.ts` alike, so it must not import either.
|
|
17
|
+
* Issuing an id is a separate act, owned by `cli-identity.ts`.
|
|
18
|
+
*/
|
|
19
|
+
const keychain_1 = require("./keychain");
|
|
20
|
+
/**
|
|
21
|
+
* The operator's `OVERSKY_CLI_ID` / `SKRR_CLI_ID`, when set. The legacy spelling
|
|
22
|
+
* wins when both are set, as it does for every other overlay variable
|
|
23
|
+
* (`config.ts`, `envOverlayValue`); the config overlay reads THIS function.
|
|
24
|
+
*/
|
|
25
|
+
function envCliIdOverride(env = process.env) {
|
|
26
|
+
const legacy = env.OVERSKY_CLI_ID?.trim();
|
|
27
|
+
if (legacy)
|
|
28
|
+
return legacy;
|
|
29
|
+
const renamed = env.SKRR_CLI_ID?.trim();
|
|
30
|
+
return renamed ? renamed : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** True when this process runs a named profile rather than the default one. */
|
|
33
|
+
function isNamedProfile() {
|
|
34
|
+
const profile = (0, keychain_1.getActiveProfile)();
|
|
35
|
+
return Boolean(profile) && profile !== keychain_1.DEFAULT_PROFILE;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Precedence:
|
|
39
|
+
*
|
|
40
|
+
* 1. `OVERSKY_CLI_ID` / `SKRR_CLI_ID` — the operator said so;
|
|
41
|
+
* 2. `profileCliId` — this named profile was issued its own identity;
|
|
42
|
+
* 3. `cliId` — the machine identity, which the default profile and every
|
|
43
|
+
* profile whose session predates profile identities keep presenting.
|
|
44
|
+
*
|
|
45
|
+
* Never mints: a read must not change who this config is.
|
|
46
|
+
*/
|
|
47
|
+
function resolveCliId(cfg) {
|
|
48
|
+
const env = envCliIdOverride();
|
|
49
|
+
if (env)
|
|
50
|
+
return env;
|
|
51
|
+
const profileScoped = cfg.profileCliId?.trim();
|
|
52
|
+
if (profileScoped)
|
|
53
|
+
return profileScoped;
|
|
54
|
+
const machine = cfg.cliId?.trim();
|
|
55
|
+
return machine ? machine : undefined;
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type CliConfig } from './config';
|
|
2
|
+
/**
|
|
3
|
+
* A named profile's own identity. Deterministic in (host, user, config root,
|
|
4
|
+
* profile), so a profile that loses its config file and signs in again is
|
|
5
|
+
* issued the same id rather than scattering families, and two profiles — or the
|
|
6
|
+
* same profile name under two config roots — never share one. Same `cli_<12hex>`
|
|
7
|
+
* shape as the machine id: the server requires the `cli_` namespace.
|
|
8
|
+
*/
|
|
9
|
+
export declare function profileScopedCliId(profile?: string, root?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Does this profile hold a credential it would present? Its own backend slot
|
|
12
|
+
* (ownership-checked, any origin) or a pre-migration token still in the config.
|
|
13
|
+
*
|
|
14
|
+
* A read that FAILS answers true: "keep the id you already present" is the
|
|
15
|
+
* outcome that cannot log anyone out, so doubt falls on that side.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasStoredCredential(cfg: CliConfig): boolean;
|
|
18
|
+
export interface LoginCliIdPlan {
|
|
19
|
+
/** The id this login presents. */
|
|
20
|
+
cliId: string;
|
|
21
|
+
/**
|
|
22
|
+
* `env`: the operator's override. `profile`: an id this profile was issued
|
|
23
|
+
* earlier. `issued`: issued now, to a named profile holding no credential.
|
|
24
|
+
* `machine`: the machine id (the default profile, or a profile whose session
|
|
25
|
+
* was minted under it).
|
|
26
|
+
*/
|
|
27
|
+
source: 'env' | 'profile' | 'issued' | 'machine';
|
|
28
|
+
/** Present when the plan chose an id that is not yet on disk; persist it. */
|
|
29
|
+
updatedConfig?: CliConfig;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decide the id a login is about to present. Pure apart from the credential
|
|
33
|
+
* read; the caller persists `updatedConfig` (see `cliIdForLogin`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function planLoginCliId(cfg: CliConfig, deps?: {
|
|
36
|
+
hasStoredCredential?: (cfg: CliConfig) => boolean;
|
|
37
|
+
}): LoginCliIdPlan;
|
|
38
|
+
/**
|
|
39
|
+
* The id an interactive login presents, persisted BEFORE the exchange so a
|
|
40
|
+
* retry — or a sibling process — presents the same one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function cliIdForLogin(cfg?: CliConfig): string;
|
|
43
|
+
/**
|
|
44
|
+
* Record that this profile now presents `cliId` — a restored (refresh token,
|
|
45
|
+
* cliId) pair. A named profile records it as its own identity; writing the
|
|
46
|
+
* machine field from a named profile would re-identify the default profile and
|
|
47
|
+
* every profile still on the machine id, whose next refresh would be refused.
|
|
48
|
+
*/
|
|
49
|
+
export declare function persistPresentedCliId(cfg: CliConfig, cliId: string): void;
|