@skrr-ai/cli 0.1.66 → 0.1.68
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/commands/logout.js +32 -63
- package/dist/commands/store/admissions/approve.js +7 -5
- package/dist/commands/store/admissions/list.js +17 -3
- package/dist/commands/store/admissions/refuse.js +7 -5
- package/dist/commands/store/install.js +5 -5
- package/dist/lib/auth-core-init.js +21 -15
- package/dist/lib/keychain.d.ts +7 -5
- package/dist/lib/keychain.js +10 -10
- package/dist/lib/refresh.d.ts +2 -0
- package/dist/lib/refresh.js +20 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +36 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +36 -4
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +9 -1
- package/oclif.manifest.json +22360 -22356
- package/package.json +2 -2
package/dist/commands/logout.js
CHANGED
|
@@ -35,9 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.shouldSkipLocalLogout = shouldSkipLocalLogout;
|
|
37
37
|
const core_1 = require("@oclif/core");
|
|
38
|
-
const fs = __importStar(require("node:fs"));
|
|
39
|
-
const os = __importStar(require("node:os"));
|
|
40
|
-
const path = __importStar(require("node:path"));
|
|
41
38
|
const base_command_1 = require("../base-command");
|
|
42
39
|
const config_1 = require("../lib/config");
|
|
43
40
|
const first_party_harness_managed_1 = require("../lib/first-party-harness-managed");
|
|
@@ -112,21 +109,6 @@ function shouldSkipLocalLogout(input) {
|
|
|
112
109
|
function platformManagedHandoff() {
|
|
113
110
|
return (0, dedicated_service_1.runningOnDedicatedRuntimeGuest)() && Boolean((0, daemonBroker_1.findBrokeredHandoffDescriptor)());
|
|
114
111
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Whether this computer has a configured skrr background service — the file
|
|
117
|
-
* `skrr daemon install` writes, which a logout deliberately does not remove
|
|
118
|
-
* (the service's OWN credential is what becomes unreadable; OSK-12483).
|
|
119
|
-
* The daemon's profile dir is `~/.skrr/profiles/<name>` for every profile,
|
|
120
|
-
* including `default`.
|
|
121
|
-
*/
|
|
122
|
-
function daemonConfigPresent(profile) {
|
|
123
|
-
try {
|
|
124
|
-
return fs.existsSync(path.join(os.homedir(), '.skrr', 'profiles', profile, 'daemon.json'));
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
112
|
const SIGN_IN_AGAIN = 'To sign in again, run `skrr login`.';
|
|
131
113
|
class Logout extends base_command_1.BaseCommand {
|
|
132
114
|
static description = 'Sign out and clear locally-stored tokens. ' +
|
|
@@ -214,39 +196,36 @@ class Logout extends base_command_1.BaseCommand {
|
|
|
214
196
|
(0, auth_storage_1.clearAllBackends)();
|
|
215
197
|
// Sweep the credentials this product wrote under its PREVIOUS brand.
|
|
216
198
|
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
/* best-effort */
|
|
199
|
+
// Ordinary logout clears retired CLI slots only. The explicit
|
|
200
|
+
// all-devices operation also clears retired daemon credentials and KEKs.
|
|
201
|
+
// Removing those during a CLI-only logout would break a running daemon's
|
|
202
|
+
// next restart or refresh (OSK-12483).
|
|
203
|
+
const sweptLegacy = keychain.deleteLegacyBrandEntries({
|
|
204
|
+
includeMachineCredentials: flags['all-devices'],
|
|
205
|
+
});
|
|
206
|
+
// Device identity and the wrapped DEK belong to the daemon as well as the
|
|
207
|
+
// CLI. Preserve both for a CLI-only logout. An explicit all-devices logout
|
|
208
|
+
// revokes daemon sessions and may then clear this profile's machine keys.
|
|
209
|
+
if (flags['all-devices']) {
|
|
210
|
+
const profile = keychain.getActiveProfile();
|
|
211
|
+
try {
|
|
212
|
+
await (0, device_identity_1.resetDeviceIdentity)({ profile, clearOnDisk: true });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* best-effort */
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
(0, device_identity_1.clearEnrollmentMarker)(profile);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* best-effort */
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await (0, cred_envelope_1.resetCredEnvelope)({ profile, clearOnDisk: true });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* best-effort */
|
|
228
|
+
}
|
|
250
229
|
}
|
|
251
230
|
// Strip any stray token/refreshToken from cli-config.json too —
|
|
252
231
|
// `migrateAuthOutOfConfig` should have done this at init time, but
|
|
@@ -300,18 +279,8 @@ class Logout extends base_command_1.BaseCommand {
|
|
|
300
279
|
}
|
|
301
280
|
else {
|
|
302
281
|
this.log(SIGN_IN_AGAIN);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// running daemon keeps working on its in-memory copy, but its next start
|
|
306
|
-
// cannot read the stored one and needs `skrr daemon login` — a plain
|
|
307
|
-
// `skrr login` mints a cli-scope credential and does not re-enroll it.
|
|
308
|
-
// Say that now, while the person is still looking at a screen; otherwise
|
|
309
|
-
// the daemon simply never comes back and nothing anywhere names why.
|
|
310
|
-
// `daemon.json` is written when the service is installed/configured and
|
|
311
|
-
// survives this logout.
|
|
312
|
-
if (daemonConfigPresent(keychain.getActiveProfile())) {
|
|
313
|
-
this.log("This also signs out this computer's skrr background service — it will need " +
|
|
314
|
-
'`skrr daemon login` before it runs again.');
|
|
282
|
+
if (!flags['all-devices']) {
|
|
283
|
+
this.log('The skrr background service keeps its own sign-in and may provide CLI access.');
|
|
315
284
|
}
|
|
316
285
|
}
|
|
317
286
|
if (sweptLegacy.length > 0) {
|
|
@@ -13,6 +13,7 @@ class StoreAdmissionsApprove extends base_command_1.BaseCommand {
|
|
|
13
13
|
static examples = [
|
|
14
14
|
'<%= config.bin %> store admissions approve ar_abc123',
|
|
15
15
|
'<%= config.bin %> store admissions approve skill_abc123 --kind skill',
|
|
16
|
+
'<%= config.bin %> store admissions approve store_release_abc123 --kind artifact',
|
|
16
17
|
'<%= config.bin %> store admissions approve store_release_abc123 --artifact',
|
|
17
18
|
];
|
|
18
19
|
static args = {
|
|
@@ -25,8 +26,8 @@ class StoreAdmissionsApprove extends base_command_1.BaseCommand {
|
|
|
25
26
|
static flags = {
|
|
26
27
|
artifact: core_1.Flags.boolean({ description: 'The id is a Skill or Action artifact release' }),
|
|
27
28
|
kind: core_1.Flags.string({
|
|
28
|
-
description: 'The id is a listing
|
|
29
|
-
options: ['skill', 'action'],
|
|
29
|
+
description: 'The id is a skill/action listing or a Store artifact release',
|
|
30
|
+
options: ['skill', 'action', 'artifact'],
|
|
30
31
|
}),
|
|
31
32
|
note: core_1.Flags.string({ description: 'Decision note' }),
|
|
32
33
|
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
@@ -35,9 +36,10 @@ class StoreAdmissionsApprove extends base_command_1.BaseCommand {
|
|
|
35
36
|
this.requireAuth();
|
|
36
37
|
const { args, flags } = await this.parse(StoreAdmissionsApprove);
|
|
37
38
|
try {
|
|
38
|
-
if (flags.artifact && flags.kind)
|
|
39
|
-
this.error('Use either --artifact or --kind');
|
|
40
|
-
|
|
39
|
+
if (flags.artifact && flags.kind && flags.kind !== 'artifact') {
|
|
40
|
+
this.error('Use either --artifact or a listing --kind');
|
|
41
|
+
}
|
|
42
|
+
const result = flags.artifact || flags.kind === 'artifact'
|
|
41
43
|
? await data_provider_1.dataService.approveStoreArtifactAdmission(args.id, flags.note)
|
|
42
44
|
: flags.kind
|
|
43
45
|
? await data_provider_1.dataService.approveStoreListingAdmission(flags.kind, args.id, flags.note)
|
|
@@ -9,7 +9,7 @@ const base_command_1 = require("../../../base-command");
|
|
|
9
9
|
* not the publisher.
|
|
10
10
|
*/
|
|
11
11
|
class StoreAdmissionsList extends base_command_1.BaseCommand {
|
|
12
|
-
static description = 'List releases and listings awaiting capability-tiered review (store_curation)';
|
|
12
|
+
static description = 'List Agent releases, artifact releases and listings awaiting capability-tiered review (store_curation)';
|
|
13
13
|
static examples = [
|
|
14
14
|
'<%= config.bin %> store admissions list',
|
|
15
15
|
'<%= config.bin %> store admissions list --state refused --json',
|
|
@@ -28,8 +28,22 @@ class StoreAdmissionsList extends base_command_1.BaseCommand {
|
|
|
28
28
|
const result = await data_provider_1.dataService.getStoreReleaseAdmissions({
|
|
29
29
|
state: flags.state === 'refused' ? 'refused' : 'pending_review',
|
|
30
30
|
});
|
|
31
|
+
const artifacts = [...(result.artifacts || [])];
|
|
32
|
+
let after = result.artifactNextCursor;
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
while (after) {
|
|
35
|
+
if (seen.has(after))
|
|
36
|
+
throw new Error('Artifact admission queue returned a repeated cursor');
|
|
37
|
+
seen.add(after);
|
|
38
|
+
const page = await data_provider_1.dataService.getStoreReleaseAdmissions({
|
|
39
|
+
state: flags.state === 'refused' ? 'refused' : 'pending_review',
|
|
40
|
+
artifactAfter: after,
|
|
41
|
+
});
|
|
42
|
+
artifacts.push(...(page.artifacts || []));
|
|
43
|
+
after = page.artifactNextCursor;
|
|
44
|
+
}
|
|
31
45
|
if (flags.json) {
|
|
32
|
-
this.log(JSON.stringify(result, null, 2));
|
|
46
|
+
this.log(JSON.stringify({ ...result, artifacts, artifactNextCursor: null }, null, 2));
|
|
33
47
|
return;
|
|
34
48
|
}
|
|
35
49
|
const rows = [
|
|
@@ -45,7 +59,7 @@ class StoreAdmissionsList extends base_command_1.BaseCommand {
|
|
|
45
59
|
name: l.name || '',
|
|
46
60
|
facets: (l.admission?.facets || []).join(','),
|
|
47
61
|
})),
|
|
48
|
-
...
|
|
62
|
+
...artifacts.map((a) => ({
|
|
49
63
|
kind: `${a.entityType} artifact`,
|
|
50
64
|
id: a.releaseId,
|
|
51
65
|
name: `${a.entityId} v${a.versionLabel}`,
|
|
@@ -12,6 +12,7 @@ class StoreAdmissionsRefuse extends base_command_1.BaseCommand {
|
|
|
12
12
|
static examples = [
|
|
13
13
|
'<%= config.bin %> store admissions refuse ar_abc123 --note "unbounded network egress"',
|
|
14
14
|
'<%= config.bin %> store admissions refuse act_abc123 --kind action --note "reason"',
|
|
15
|
+
'<%= config.bin %> store admissions refuse store_release_abc123 --kind artifact --note "reason"',
|
|
15
16
|
'<%= config.bin %> store admissions refuse store_release_abc123 --artifact --note "reason"',
|
|
16
17
|
];
|
|
17
18
|
static args = {
|
|
@@ -24,8 +25,8 @@ class StoreAdmissionsRefuse extends base_command_1.BaseCommand {
|
|
|
24
25
|
static flags = {
|
|
25
26
|
artifact: core_1.Flags.boolean({ description: 'The id is a Skill or Action artifact release' }),
|
|
26
27
|
kind: core_1.Flags.string({
|
|
27
|
-
description: 'The id is a listing
|
|
28
|
-
options: ['skill', 'action'],
|
|
28
|
+
description: 'The id is a skill/action listing or a Store artifact release',
|
|
29
|
+
options: ['skill', 'action', 'artifact'],
|
|
29
30
|
}),
|
|
30
31
|
note: core_1.Flags.string({ description: 'Why the refusal — required', required: true }),
|
|
31
32
|
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
@@ -34,9 +35,10 @@ class StoreAdmissionsRefuse extends base_command_1.BaseCommand {
|
|
|
34
35
|
this.requireAuth();
|
|
35
36
|
const { args, flags } = await this.parse(StoreAdmissionsRefuse);
|
|
36
37
|
try {
|
|
37
|
-
if (flags.artifact && flags.kind)
|
|
38
|
-
this.error('Use either --artifact or --kind');
|
|
39
|
-
|
|
38
|
+
if (flags.artifact && flags.kind && flags.kind !== 'artifact') {
|
|
39
|
+
this.error('Use either --artifact or a listing --kind');
|
|
40
|
+
}
|
|
41
|
+
const result = flags.artifact || flags.kind === 'artifact'
|
|
40
42
|
? await data_provider_1.dataService.refuseStoreArtifactAdmission(args.id, flags.note || '')
|
|
41
43
|
: flags.kind
|
|
42
44
|
? await data_provider_1.dataService.refuseStoreListingAdmission(flags.kind, args.id, flags.note || '')
|
|
@@ -60,18 +60,18 @@ class StoreInstall extends base_command_1.BaseCommand {
|
|
|
60
60
|
// The server still refuses a second install of a listing you own — this is
|
|
61
61
|
// the friendly read that says so before a quote or a prompt (OSK-12287:
|
|
62
62
|
// `installs` is the read model; `purchased` answers membership cheaply).
|
|
63
|
+
let alreadyInstalled = false;
|
|
63
64
|
try {
|
|
64
65
|
const owned = await data_provider_1.dataService.getPurchasedAgents();
|
|
65
|
-
|
|
66
|
-
this.error(`${args.agentId} is already installed. See it: skrr store updates, or update it: skrr store update ${args.agentId}`, { exit: 1 });
|
|
67
|
-
}
|
|
66
|
+
alreadyInstalled = (owned?.purchasedAgentIds ?? []).includes(args.agentId);
|
|
68
67
|
}
|
|
69
68
|
catch (err) {
|
|
70
69
|
// A read failure must not block an install — the server re-checks anyway.
|
|
71
|
-
if (err.exit)
|
|
72
|
-
throw err;
|
|
73
70
|
this.warn(`Could not check what you own (${err.message}); continuing.`);
|
|
74
71
|
}
|
|
72
|
+
if (alreadyInstalled) {
|
|
73
|
+
this.error(`${args.agentId} is already installed. See it: skrr store updates, or update it: skrr store update ${args.agentId}`, { exit: 1 });
|
|
74
|
+
}
|
|
75
75
|
// What installing would charge NOW, resolved by the server the way the
|
|
76
76
|
// install resolves it (OSK-12240). The install then pins what was shown, so
|
|
77
77
|
// a listing whose price moved in between is refused rather than charged.
|
|
@@ -17,11 +17,10 @@ exports.__resetPrefetchCliAuthForTest = __resetPrefetchCliAuthForTest;
|
|
|
17
17
|
* Runs as a side-effect import from base-command.ts so every command path
|
|
18
18
|
* (including future ones) gets the same DI setup. Idempotent and cheap.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* Runtime scope:
|
|
21
21
|
* - Logger: a thin shim over `console`. CLI is a one-shot process; we
|
|
22
22
|
* don't ship winston for this.
|
|
23
|
-
* - configDir:
|
|
24
|
-
* ~/.skrr/ with a one-shot read-shim migration.
|
|
23
|
+
* - configDir: the shared ~/.skrr/ root, with profile state below it.
|
|
25
24
|
* - Kill switches: respect OVERSKY_DISABLE_REFRESH_SCHEDULER for parity
|
|
26
25
|
* with the daemon's rollback knob.
|
|
27
26
|
* - binaryName: 'skrr' so reauth banners say `skrr login` not `oversky login`.
|
|
@@ -42,6 +41,7 @@ const auth_core_1 = require("@skrr-ai/auth-core");
|
|
|
42
41
|
const cred_envelope_1 = require("./cred-envelope");
|
|
43
42
|
const device_identity_1 = require("./device-identity");
|
|
44
43
|
const auth_storage_1 = require("./auth-storage");
|
|
44
|
+
const keychain_1 = require("./keychain");
|
|
45
45
|
function envFlag(name) {
|
|
46
46
|
const v = process.env[name];
|
|
47
47
|
return v === '1' || v === 'true' || v === 'TRUE';
|
|
@@ -165,12 +165,6 @@ const cliLogger = {
|
|
|
165
165
|
},
|
|
166
166
|
error: (...args) => console.error('[auth-core error]', ...args),
|
|
167
167
|
};
|
|
168
|
-
/**
|
|
169
|
-
* Profile name in shared on-disk paths. Pre-Phase-K every consumer
|
|
170
|
-
* resolves to `'default'`; matches `daemon/src/auth.ts:DEFAULT_PROFILE`
|
|
171
|
-
* so cross-binary file paths align byte-for-byte.
|
|
172
|
-
*/
|
|
173
|
-
const DEFAULT_PROFILE = 'default';
|
|
174
168
|
const DEDICATED_AUTH_GENERATION_MARKER = 'dedicated-runtime-auth-generation';
|
|
175
169
|
/**
|
|
176
170
|
* A Dedicated Runtime's workspace survives replacement but `/etc/machine-id`
|
|
@@ -192,7 +186,11 @@ async function prepareDedicatedRuntimeAuthGeneration(options = {}) {
|
|
|
192
186
|
return false;
|
|
193
187
|
const configDir = options.configDir ?? (0, auth_core_1.resolveConfigRoot)();
|
|
194
188
|
node_fs_1.default.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
195
|
-
const
|
|
189
|
+
const profile = (0, keychain_1.getActiveProfile)();
|
|
190
|
+
const markerName = profile === keychain_1.DEFAULT_PROFILE
|
|
191
|
+
? DEDICATED_AUTH_GENERATION_MARKER
|
|
192
|
+
: `${DEDICATED_AUTH_GENERATION_MARKER}.${profile}`;
|
|
193
|
+
const markerPath = node_path_1.default.join(configDir, markerName);
|
|
196
194
|
const release = await proper_lockfile_1.default.lock(configDir, {
|
|
197
195
|
lockfilePath: `${markerPath}.lock`,
|
|
198
196
|
realpath: false,
|
|
@@ -212,12 +210,12 @@ async function prepareDedicatedRuntimeAuthGeneration(options = {}) {
|
|
|
212
210
|
(options.clearStoredAuth ?? auth_storage_1.clearAllBackends)();
|
|
213
211
|
await (options.clearDeviceIdentity ??
|
|
214
212
|
(() => (0, device_identity_1.resetDeviceIdentity)({
|
|
215
|
-
profile
|
|
213
|
+
profile,
|
|
216
214
|
clearOnDisk: true,
|
|
217
215
|
})))();
|
|
218
216
|
await (options.clearCredentialEnvelope ??
|
|
219
217
|
(() => (0, cred_envelope_1.resetCredEnvelope)({
|
|
220
|
-
profile
|
|
218
|
+
profile,
|
|
221
219
|
clearOnDisk: true,
|
|
222
220
|
})))();
|
|
223
221
|
const temporary = `${markerPath}.tmp.${process.pid}.${Date.now()}`;
|
|
@@ -298,6 +296,7 @@ function ensureAuthCoreConfigured() {
|
|
|
298
296
|
let _prefetchPromise = null;
|
|
299
297
|
let _credEnvelopeInitPromise = null;
|
|
300
298
|
let _credEnvelopeInitStartedAt = 0;
|
|
299
|
+
let _prefetchProfile = null;
|
|
301
300
|
class CliCredentialInitializationError extends Error {
|
|
302
301
|
code = 'CREDENTIAL_INITIALIZATION_UNAVAILABLE';
|
|
303
302
|
exitCode = 75;
|
|
@@ -342,15 +341,21 @@ function encryptedCredentialInitError(timedOut, cause) {
|
|
|
342
341
|
'The credential remains protected; unlock or repair the OS credential store and retry.');
|
|
343
342
|
}
|
|
344
343
|
function ensureCliAuthPrefetchStarted() {
|
|
345
|
-
|
|
344
|
+
const profile = (0, keychain_1.getActiveProfile)();
|
|
345
|
+
if (_prefetchPromise) {
|
|
346
|
+
if (_prefetchProfile !== profile) {
|
|
347
|
+
throw new CliCredentialInitializationError(`Credential initialization already belongs to profile ${_prefetchProfile}; start a new skrr process for profile ${profile}.`);
|
|
348
|
+
}
|
|
346
349
|
return;
|
|
350
|
+
}
|
|
351
|
+
_prefetchProfile = profile;
|
|
347
352
|
_credEnvelopeInitStartedAt = Date.now();
|
|
348
|
-
const envelopeInit = (0, cred_envelope_1.initCredEnvelope)(
|
|
353
|
+
const envelopeInit = (0, cred_envelope_1.initCredEnvelope)(profile);
|
|
349
354
|
_credEnvelopeInitPromise = envelopeInit;
|
|
350
355
|
_prefetchPromise = (async () => {
|
|
351
356
|
try {
|
|
352
357
|
await envelopeInit;
|
|
353
|
-
await (0, device_identity_1.initDeviceIdentity)(
|
|
358
|
+
await (0, device_identity_1.initDeviceIdentity)(profile);
|
|
354
359
|
// The CLI deliberately does NOT enroll a device key with the server
|
|
355
360
|
// (OSK-12063). Enrollment pins the FIRST key presented on a machine's
|
|
356
361
|
// Daemon row, and every later refresh signed by a different key is
|
|
@@ -431,6 +436,7 @@ function __resetPrefetchCliAuthForTest() {
|
|
|
431
436
|
_prefetchPromise = null;
|
|
432
437
|
_credEnvelopeInitPromise = null;
|
|
433
438
|
_credEnvelopeInitStartedAt = 0;
|
|
439
|
+
_prefetchProfile = null;
|
|
434
440
|
}
|
|
435
441
|
// Eager-configure on first import. Cheap and avoids ordering pitfalls in
|
|
436
442
|
// any future command that calls auth-core helpers from outside a Command.
|
package/dist/lib/keychain.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Default profile when no profile flag or environment override is present. */
|
|
2
2
|
export declare const DEFAULT_PROFILE = "default";
|
|
3
3
|
export declare function setActiveProfile(profile: string): void;
|
|
4
4
|
/**
|
|
@@ -102,11 +102,13 @@ export declare function deleteRefreshToken(): void;
|
|
|
102
102
|
* Returns the services it actually removed something from, so the caller can
|
|
103
103
|
* say what happened instead of destroying credentials silently.
|
|
104
104
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
105
|
+
* Ordinary logout removes only retired CLI slots. Machine credentials and
|
|
106
|
+
* KEKs are shared with the daemon and are removed only by --all-devices.
|
|
107
|
+
* That broader mode also signs out an installed retired `oversky` daemon.
|
|
108
108
|
*/
|
|
109
|
-
export declare function deleteLegacyBrandEntries(
|
|
109
|
+
export declare function deleteLegacyBrandEntries(options?: {
|
|
110
|
+
includeMachineCredentials?: boolean;
|
|
111
|
+
}): string[];
|
|
110
112
|
/**
|
|
111
113
|
* Remove every CLI keychain slot for the active profile. Used by `skrr logout`.
|
|
112
114
|
* Safe to call on a clean system — missing items are not an error.
|
package/dist/lib/keychain.js
CHANGED
|
@@ -67,10 +67,8 @@ exports.resolveProfileFromArgv = resolveProfileFromArgv;
|
|
|
67
67
|
* Mirrors `daemon/src/keychain.ts` but simpler:
|
|
68
68
|
* - No prefetch: the CLI is a short-lived foreground process; parallelizing
|
|
69
69
|
* the keychain probe with other startup work isn't worth the code.
|
|
70
|
-
* - Namespaced `ai.skrr.cli.<profile>`
|
|
71
|
-
*
|
|
72
|
-
* the suffix in now makes the future migration a rename instead of a
|
|
73
|
-
* schema change.
|
|
70
|
+
* - Namespaced `ai.skrr.cli.<profile>` so each selected profile keeps
|
|
71
|
+
* its own CLI refresh family.
|
|
74
72
|
* - Distinct service from the daemon (`ai.skrr.cli.*` vs.
|
|
75
73
|
* `ai.skrr.daemon.*`). The CLI holds `scope: 'cli'` tokens; the
|
|
76
74
|
* daemon holds `scope: 'daemon'`. They must never read each other's
|
|
@@ -121,7 +119,7 @@ const cred_envelope_1 = require("./cred-envelope");
|
|
|
121
119
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
122
120
|
/** Service-name prefix. Always followed by `.<profile>`. */
|
|
123
121
|
const SERVICE_BASE = 'ai.skrr.cli';
|
|
124
|
-
/**
|
|
122
|
+
/** Default profile when no profile flag or environment override is present. */
|
|
125
123
|
exports.DEFAULT_PROFILE = 'default';
|
|
126
124
|
const ACCOUNT_TOKEN = 'cli-token';
|
|
127
125
|
const ACCOUNT_EXPIRES = 'cli-token-expires';
|
|
@@ -619,14 +617,16 @@ function deleteEntireService(service) {
|
|
|
619
617
|
* Returns the services it actually removed something from, so the caller can
|
|
620
618
|
* say what happened instead of destroying credentials silently.
|
|
621
619
|
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
620
|
+
* Ordinary logout removes only retired CLI slots. Machine credentials and
|
|
621
|
+
* KEKs are shared with the daemon and are removed only by --all-devices.
|
|
622
|
+
* That broader mode also signs out an installed retired `oversky` daemon.
|
|
625
623
|
*/
|
|
626
|
-
function deleteLegacyBrandEntries() {
|
|
624
|
+
function deleteLegacyBrandEntries(options = {}) {
|
|
627
625
|
if (!isAvailable())
|
|
628
626
|
return [];
|
|
629
|
-
return discoverLegacyBrandServices()
|
|
627
|
+
return discoverLegacyBrandServices()
|
|
628
|
+
.filter((service) => options.includeMachineCredentials === true || /^(?:ai|com)\.oversky\.cli\./.test(service))
|
|
629
|
+
.filter((service) => deleteEntireService(service));
|
|
630
630
|
}
|
|
631
631
|
/**
|
|
632
632
|
* Remove every CLI keychain slot for the active profile. Used by `skrr logout`.
|
package/dist/lib/refresh.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface EnsureFreshCliCredentialResult {
|
|
|
45
45
|
/** True when we adopted a fresher token another process already wrote. */
|
|
46
46
|
adoptedStoredToken?: boolean;
|
|
47
47
|
pair?: TokenPair;
|
|
48
|
+
/** Stable diagnostic code; never contains token or server response text. */
|
|
49
|
+
refreshFailureCode?: string;
|
|
48
50
|
skippedReason?: 'no_token' | 'ci_token' | 'external_source' | 'env_override' | 'bare_mode' | 'fresh' | 'no_refresh_token' | 'refresh_failed';
|
|
49
51
|
}
|
|
50
52
|
/**
|
package/dist/lib/refresh.js
CHANGED
|
@@ -58,6 +58,7 @@ class CliRefreshUnavailableError extends Error {
|
|
|
58
58
|
}
|
|
59
59
|
exports.CliRefreshUnavailableError = CliRefreshUnavailableError;
|
|
60
60
|
let ensureInFlight = null;
|
|
61
|
+
let reportedPermanentRefreshFailure = false;
|
|
61
62
|
function expiryFromToken(token) {
|
|
62
63
|
if (!token)
|
|
63
64
|
return undefined;
|
|
@@ -158,8 +159,25 @@ async function ensureFreshCliCredential(opts = {}) {
|
|
|
158
159
|
pair,
|
|
159
160
|
};
|
|
160
161
|
}
|
|
161
|
-
catch {
|
|
162
|
-
|
|
162
|
+
catch (err) {
|
|
163
|
+
const refreshFailureCode = err instanceof auth_core_1.PermanentAuthFailure
|
|
164
|
+
? err.code
|
|
165
|
+
: err instanceof auth_core_1.TransientAuthFailure
|
|
166
|
+
? 'TRANSIENT_REFRESH_FAILURE'
|
|
167
|
+
: err instanceof CliRefreshUnavailableError
|
|
168
|
+
? 'LOCAL_REFRESH_UNAVAILABLE'
|
|
169
|
+
: 'LOCAL_REFRESH_ERROR';
|
|
170
|
+
if (err instanceof auth_core_1.PermanentAuthFailure && !reportedPermanentRefreshFailure) {
|
|
171
|
+
reportedPermanentRefreshFailure = true;
|
|
172
|
+
console.error(`[skrr/auth] Stored CLI session refresh was rejected (${refreshFailureCode}). ` +
|
|
173
|
+
'The background service may still provide access; run `skrr login` if it cannot.');
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
credential,
|
|
177
|
+
refreshed: false,
|
|
178
|
+
skippedReason: 'refresh_failed',
|
|
179
|
+
refreshFailureCode,
|
|
180
|
+
};
|
|
163
181
|
}
|
|
164
182
|
finally {
|
|
165
183
|
ensureInFlight = null;
|
|
@@ -10,7 +10,7 @@ type DeviceIdentityState = {
|
|
|
10
10
|
profile: string;
|
|
11
11
|
envelopeProtected: boolean;
|
|
12
12
|
};
|
|
13
|
-
type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed';
|
|
13
|
+
type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key';
|
|
14
14
|
/** @internal Test seam — force-set state. */
|
|
15
15
|
export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
|
|
16
16
|
/** @internal Test seam — read state. */
|
|
@@ -42,11 +42,13 @@ export declare function describeDeviceKeySplit(profile: string): {
|
|
|
42
42
|
* 2. Generate fresh keypair and persist. Failure to persist is a hard
|
|
43
43
|
* error landing in disabled+persist_failed.
|
|
44
44
|
*/
|
|
45
|
-
export declare function initDeviceIdentity(profile: string
|
|
45
|
+
export declare function initDeviceIdentity(profile: string, options?: {
|
|
46
|
+
recoverUnreadable?: boolean;
|
|
47
|
+
}): Promise<void>;
|
|
46
48
|
/**
|
|
47
|
-
* Reset state. With `clearOnDisk=true` deletes both keypair files
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
|
|
50
|
+
* Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
|
|
51
|
+
* the daemon's pinned identity.
|
|
50
52
|
*/
|
|
51
53
|
export declare function resetDeviceIdentity(opts?: {
|
|
52
54
|
profile?: string;
|
|
@@ -100,6 +100,26 @@ function publicKeyPath(profile) {
|
|
|
100
100
|
function privateKeyPath(profile) {
|
|
101
101
|
return node_path_1.default.join(profileConfigDir(profile), PRIVATE_KEY_FILENAME);
|
|
102
102
|
}
|
|
103
|
+
function storedKeyMaterialExists(profile) {
|
|
104
|
+
let files;
|
|
105
|
+
try {
|
|
106
|
+
files = [publicKeyPath(profile), privateKeyPath(profile), enrollmentMarkerPath(profile)];
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
try {
|
|
113
|
+
node_fs_1.default.lstatSync(file);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err.code !== 'ENOENT')
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
103
123
|
function enrollmentMarkerPath(profile) {
|
|
104
124
|
return node_path_1.default.join(profileConfigDir(profile), ENROLLMENT_MARKER_FILENAME);
|
|
105
125
|
}
|
|
@@ -339,7 +359,7 @@ async function consolidateDeviceIdentity(profile) {
|
|
|
339
359
|
* 2. Generate fresh keypair and persist. Failure to persist is a hard
|
|
340
360
|
* error landing in disabled+persist_failed.
|
|
341
361
|
*/
|
|
342
|
-
async function initDeviceIdentity(profile) {
|
|
362
|
+
async function initDeviceIdentity(profile, options = {}) {
|
|
343
363
|
if (_state.kind !== 'uninit')
|
|
344
364
|
return;
|
|
345
365
|
if (typeof profile !== 'string' || profile.length === 0) {
|
|
@@ -374,6 +394,18 @@ async function initDeviceIdentity(profile) {
|
|
|
374
394
|
});
|
|
375
395
|
return;
|
|
376
396
|
}
|
|
397
|
+
// A first install has neither file. If either file already exists, a null
|
|
398
|
+
// load is a lost, corrupt or temporarily unreadable identity. Replacing it
|
|
399
|
+
// during ordinary boot would make the next refresh sign with a key the
|
|
400
|
+
// server has never pinned, turning a local read failure into an unexpected
|
|
401
|
+
// DEVICE_KEY_MISMATCH. Only an explicit interactive login may replace it;
|
|
402
|
+
// that login proves the new key and re-pins it before issuing credentials.
|
|
403
|
+
const hasStoredKey = storedKeyMaterialExists(profile);
|
|
404
|
+
if (hasStoredKey && !options.recoverUnreadable) {
|
|
405
|
+
_state = { kind: 'disabled', reason: 'unreadable_key' };
|
|
406
|
+
emit('recovery_required', false, { profile, reason: 'stored_key_unreadable' });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
377
409
|
let fresh;
|
|
378
410
|
try {
|
|
379
411
|
fresh = (0, deviceKey_js_1.generateDeviceKeyPair)();
|
|
@@ -413,9 +445,9 @@ async function initDeviceIdentity(profile) {
|
|
|
413
445
|
});
|
|
414
446
|
}
|
|
415
447
|
/**
|
|
416
|
-
* Reset state. With `clearOnDisk=true` deletes both keypair files
|
|
417
|
-
*
|
|
418
|
-
*
|
|
448
|
+
* Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
|
|
449
|
+
* Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
|
|
450
|
+
* the daemon's pinned identity.
|
|
419
451
|
*/
|
|
420
452
|
async function resetDeviceIdentity(opts) {
|
|
421
453
|
if (opts?.clearOnDisk && opts.profile) {
|
|
@@ -10,7 +10,7 @@ type DeviceIdentityState = {
|
|
|
10
10
|
profile: string;
|
|
11
11
|
envelopeProtected: boolean;
|
|
12
12
|
};
|
|
13
|
-
type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed';
|
|
13
|
+
type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key';
|
|
14
14
|
/** @internal Test seam — force-set state. */
|
|
15
15
|
export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
|
|
16
16
|
/** @internal Test seam — read state. */
|
|
@@ -42,11 +42,13 @@ export declare function describeDeviceKeySplit(profile: string): {
|
|
|
42
42
|
* 2. Generate fresh keypair and persist. Failure to persist is a hard
|
|
43
43
|
* error landing in disabled+persist_failed.
|
|
44
44
|
*/
|
|
45
|
-
export declare function initDeviceIdentity(profile: string
|
|
45
|
+
export declare function initDeviceIdentity(profile: string, options?: {
|
|
46
|
+
recoverUnreadable?: boolean;
|
|
47
|
+
}): Promise<void>;
|
|
46
48
|
/**
|
|
47
|
-
* Reset state. With `clearOnDisk=true` deletes both keypair files
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
|
|
50
|
+
* Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
|
|
51
|
+
* the daemon's pinned identity.
|
|
50
52
|
*/
|
|
51
53
|
export declare function resetDeviceIdentity(opts?: {
|
|
52
54
|
profile?: string;
|