@skrr-ai/cli 0.1.123 → 0.1.124
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/agents/delete.d.ts +8 -0
- package/dist/commands/agents/delete.js +16 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/search/workspace.js +1 -1
- package/dist/commands/store/gallery-admissions/approve.js +1 -1
- package/dist/commands/store/gallery-admissions/preview.js +1 -1
- package/dist/commands/store/gallery-admissions/refuse.js +1 -1
- package/dist/commands/store/gallery-admissions/show.js +1 -1
- package/dist/lib/auth-core-init.d.ts +15 -2
- package/dist/lib/auth-core-init.js +47 -6
- package/dist/lib/command-hygiene.js +30 -1
- package/dist/lib/daemonBrokerRefusal.js +5 -1
- package/dist/lib/keychain.js +41 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +13 -28
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +133 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +387 -90
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +102 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +11 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +43 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +33 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +15 -37
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +133 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +536 -101
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +13 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +43 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +104 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +14 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +25 -7
- package/oclif.manifest.json +1 -1
- package/package.json +4 -1
|
@@ -49,7 +49,9 @@ var import_node_fs = __toESM(require("node:fs"), 1);
|
|
|
49
49
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
50
50
|
var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
|
|
51
51
|
var import_runtime = require("./runtime.js");
|
|
52
|
+
var import_liveState = require("./liveState.js");
|
|
52
53
|
var import_refreshClassification = require("./refreshClassification.js");
|
|
54
|
+
var import_stateFiles = require("./stateFiles.js");
|
|
53
55
|
var import_types = require("./types.js");
|
|
54
56
|
const LOCK_STALE_MS = 5e3;
|
|
55
57
|
const LOCK_RETRY_MS = 100;
|
|
@@ -125,9 +127,9 @@ async function withAuthLock(body, daemonId) {
|
|
|
125
127
|
const lockPath = authLockPath();
|
|
126
128
|
const dir = import_node_path.default.dirname(lockPath);
|
|
127
129
|
if (!import_node_fs.default.existsSync(dir))
|
|
128
|
-
|
|
130
|
+
(0, import_stateFiles.ensureStateDir)(dir, "create the auth lock directory", null);
|
|
129
131
|
if (!import_node_fs.default.existsSync(lockPath)) {
|
|
130
|
-
|
|
132
|
+
(0, import_stateFiles.writeStateFile)(lockPath, "", { mode: 384 }, "create the auth lock");
|
|
131
133
|
}
|
|
132
134
|
const acquireStart = Date.now();
|
|
133
135
|
(0, import_runtime.emitAuthTelemetry)({
|
|
@@ -283,7 +285,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
283
285
|
body.machineUuid = options.machineUuid;
|
|
284
286
|
}
|
|
285
287
|
let proofAttached = false;
|
|
286
|
-
function buildRequestInit() {
|
|
288
|
+
async function buildRequestInit() {
|
|
287
289
|
proofAttached = false;
|
|
288
290
|
const headers = { "Content-Type": "application/json" };
|
|
289
291
|
if (options.origin) {
|
|
@@ -292,7 +294,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
292
294
|
__refreshDiagnostics.requestAttemptCount += 1;
|
|
293
295
|
if (options.signRequestProof) {
|
|
294
296
|
try {
|
|
295
|
-
const proof = options.signRequestProof({
|
|
297
|
+
const proof = await options.signRequestProof({
|
|
296
298
|
method: "POST",
|
|
297
299
|
url,
|
|
298
300
|
refreshToken: currentRefreshToken
|
|
@@ -315,7 +317,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
315
317
|
}
|
|
316
318
|
let response;
|
|
317
319
|
try {
|
|
318
|
-
response = await fetch(url, buildRequestInit());
|
|
320
|
+
response = await fetch(url, await buildRequestInit());
|
|
319
321
|
(0, import_runtime.notifyResponseObserver)(response);
|
|
320
322
|
} catch (err) {
|
|
321
323
|
log.warn(`[auth/refresh] network error: ${err?.message}`);
|
|
@@ -501,11 +503,10 @@ function persistReauthState(state) {
|
|
|
501
503
|
try {
|
|
502
504
|
const dir = import_node_path.default.dirname(flagPath);
|
|
503
505
|
if (!import_node_fs.default.existsSync(dir))
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
mode: 384
|
|
507
|
-
});
|
|
506
|
+
(0, import_stateFiles.ensureStateDir)(dir, "create the re-auth latch directory");
|
|
507
|
+
(0, import_stateFiles.writeStateFile)(flagPath, JSON.stringify(state, null, 2) + "\n", { mode: 384 }, "write the re-auth latch");
|
|
508
508
|
} catch (err) {
|
|
509
|
+
(0, import_liveState.rethrowIfLiveStateRefusal)(err);
|
|
509
510
|
log.debug(`[auth/refresh] failed to persist reauth flag: ${err?.message}`);
|
|
510
511
|
}
|
|
511
512
|
}
|
|
@@ -532,8 +533,9 @@ function clearReauthState() {
|
|
|
532
533
|
try {
|
|
533
534
|
const flagPath = reauthFlagPath();
|
|
534
535
|
if (import_node_fs.default.existsSync(flagPath))
|
|
535
|
-
|
|
536
|
+
(0, import_stateFiles.removeStateFile)(flagPath, "clear the re-auth latch");
|
|
536
537
|
} catch (err) {
|
|
538
|
+
(0, import_liveState.rethrowIfLiveStateRefusal)(err);
|
|
537
539
|
log.debug(`[auth/refresh] clearReauthState: ${err?.message}`);
|
|
538
540
|
}
|
|
539
541
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stateFiles.ts — the ONE place auth-core mutates credential state on disk.
|
|
3
|
+
*
|
|
4
|
+
* Every write, delete, rename and mkdir of a wrapped DEK, a device key, an enrollment marker, a
|
|
5
|
+
* lock sentinel, a re-auth latch or a machine identity goes through a function here, and each
|
|
6
|
+
* asks `assertMayMutateLiveState` first: a test process aimed at the account's real home is
|
|
7
|
+
* refused with a named error before a byte changes (`liveState.ts` says why that cannot be left
|
|
8
|
+
* to each runner).
|
|
9
|
+
*
|
|
10
|
+
* This is the "own a control surface, or do not touch it" rule applied to the real home. The
|
|
11
|
+
* atomic writer used to exist three times (device identity, credential envelope, profile
|
|
12
|
+
* consolidation), so a guard added to one left two doors open. `stateFileBoundary.spec.ts`
|
|
13
|
+
* fails when a file other than this one starts calling a raw `fs` mutator, so a fourth copy
|
|
14
|
+
* cannot appear unguarded.
|
|
15
|
+
*
|
|
16
|
+
* Outside a test process every function here is exactly the `fs` call it wraps.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
/** `mkdir -p` for a state directory, 0700 unless `mode` says otherwise. */
|
|
20
|
+
export declare function ensureStateDir(dir: string, what: string, mode?: number | null): void;
|
|
21
|
+
/**
|
|
22
|
+
* Atomic 0600 write — tmp+fsync+rename+dir-fsync. The same shape the daemon and CLI always used,
|
|
23
|
+
* so a partial-write race between `skrr` and `skrrd` produces the same on-disk transitions.
|
|
24
|
+
*/
|
|
25
|
+
export declare function atomicWrite0600(target: string, value: Buffer | string, what: string): void;
|
|
26
|
+
/** `fs.writeFileSync` for a state file: lock sentinels, a re-auth latch, a machine identity. */
|
|
27
|
+
export declare function writeStateFile(target: string, data: string | NodeJS.ArrayBufferView, options: fs.WriteFileOptions, what: string): void;
|
|
28
|
+
/** `fs.unlinkSync` for a state file. Throws what `unlinkSync` throws, ENOENT included. */
|
|
29
|
+
export declare function removeStateFile(target: string, what: string): void;
|
|
30
|
+
/** `fs.renameSync` between state paths; both ends are checked. */
|
|
31
|
+
export declare function renameStateFile(from: string, to: string, what: string): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var stateFiles_exports = {};
|
|
30
|
+
__export(stateFiles_exports, {
|
|
31
|
+
atomicWrite0600: () => atomicWrite0600,
|
|
32
|
+
ensureStateDir: () => ensureStateDir,
|
|
33
|
+
removeStateFile: () => removeStateFile,
|
|
34
|
+
renameStateFile: () => renameStateFile,
|
|
35
|
+
writeStateFile: () => writeStateFile
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(stateFiles_exports);
|
|
38
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
39
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
40
|
+
var import_liveState = require("./liveState.js");
|
|
41
|
+
function ensureStateDir(dir, what, mode = 448) {
|
|
42
|
+
(0, import_liveState.assertMayMutateLiveState)(dir, what);
|
|
43
|
+
import_node_fs.default.mkdirSync(dir, mode === null ? { recursive: true } : { recursive: true, mode });
|
|
44
|
+
}
|
|
45
|
+
function atomicWrite0600(target, value, what) {
|
|
46
|
+
(0, import_liveState.assertMayMutateLiveState)(target, what);
|
|
47
|
+
const dir = import_node_path.default.dirname(target);
|
|
48
|
+
import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
49
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
50
|
+
const buf = typeof value === "string" ? Buffer.from(value, "utf-8") : value;
|
|
51
|
+
const fd = import_node_fs.default.openSync(tmp, "w", 384);
|
|
52
|
+
try {
|
|
53
|
+
import_node_fs.default.writeSync(fd, buf);
|
|
54
|
+
import_node_fs.default.fsyncSync(fd);
|
|
55
|
+
} finally {
|
|
56
|
+
import_node_fs.default.closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
import_node_fs.default.renameSync(tmp, target);
|
|
59
|
+
try {
|
|
60
|
+
const dirFd = import_node_fs.default.openSync(dir, "r");
|
|
61
|
+
try {
|
|
62
|
+
import_node_fs.default.fsyncSync(dirFd);
|
|
63
|
+
} finally {
|
|
64
|
+
import_node_fs.default.closeSync(dirFd);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function writeStateFile(target, data, options, what) {
|
|
70
|
+
(0, import_liveState.assertMayMutateLiveState)(target, what);
|
|
71
|
+
import_node_fs.default.writeFileSync(target, data, options);
|
|
72
|
+
}
|
|
73
|
+
function removeStateFile(target, what) {
|
|
74
|
+
(0, import_liveState.assertMayMutateLiveState)(target, what);
|
|
75
|
+
import_node_fs.default.unlinkSync(target);
|
|
76
|
+
}
|
|
77
|
+
function renameStateFile(from, to, what) {
|
|
78
|
+
(0, import_liveState.assertMayMutateLiveState)(from, what);
|
|
79
|
+
(0, import_liveState.assertMayMutateLiveState)(to, what);
|
|
80
|
+
import_node_fs.default.renameSync(from, to);
|
|
81
|
+
}
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
atomicWrite0600,
|
|
85
|
+
ensureStateDir,
|
|
86
|
+
removeStateFile,
|
|
87
|
+
renameStateFile,
|
|
88
|
+
writeStateFile
|
|
89
|
+
});
|
|
@@ -161,7 +161,11 @@ export type DaemonSupervisor = 'desktop' | 'service';
|
|
|
161
161
|
export declare function daemonSignInRemedy(opts?: {
|
|
162
162
|
/** How the daemon is run; absent when the caller cannot tell. */
|
|
163
163
|
supervisor?: DaemonSupervisor;
|
|
164
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* The exact login command for this profile (`skrrd … login`, or
|
|
166
|
+
* `skrr daemon login --profile …`), when the caller knows it. Used when the
|
|
167
|
+
* supervisor is `service` or unknown; a desktop-hosted daemon is never told one.
|
|
168
|
+
*/
|
|
165
169
|
loginCommand?: string;
|
|
166
170
|
/** The refusal was DEVICE_PROOF_PIN_MISMATCH: say that a sign-in re-pins. */
|
|
167
171
|
repinsDeviceKey?: boolean;
|
|
@@ -167,7 +167,10 @@ export function daemonSignInRemedy(opts = {}) {
|
|
|
167
167
|
const login = opts.loginCommand ? `\`${opts.loginCommand}\`` : '`skrr daemon login`';
|
|
168
168
|
return `run ${login}${repin ? ' — an interactive login re-pins the device key' : ''}`;
|
|
169
169
|
}
|
|
170
|
+
// A caller that cannot tell who runs the daemon may still know which profile it
|
|
171
|
+
// is on: `skrr daemon login` with no `--profile` signs the DEFAULT daemon in
|
|
172
|
+
// (OSK-16435), so the command it names is the one it was given.
|
|
170
173
|
return ('sign the daemon in again on the machine running it: if the Skrr desktop app runs it, quit ' +
|
|
171
|
-
|
|
174
|
+
`and reopen the app; otherwise run \`${opts.loginCommand ?? 'skrr daemon login'}\`` +
|
|
172
175
|
(repin ? ' (either one re-pins the device key)' : ''));
|
|
173
176
|
}
|
|
@@ -47,8 +47,10 @@ import lockfile from 'proper-lockfile';
|
|
|
47
47
|
import { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, isEnvelopeString as isCredEnvelopeString, generateDek, serialize as credEnvelopeSerialize, } from './credentialEnvelope.js';
|
|
48
48
|
import { getKekStrategy, KekUnavailableError, zeroizeKekCaches, } from './kek/index.js';
|
|
49
49
|
import { consolidateProfileState, profileStateDir, profileStateFilePathsToClear, resolveProfileStateFile, RETIRED_COPY_SUFFIX, } from './profileStateDir.js';
|
|
50
|
+
import { assertMayMutateLiveState, rethrowIfLiveStateRefusal } from './liveState.js';
|
|
50
51
|
import { needsReauthState } from './refresh.js';
|
|
51
52
|
import { emitAuthTelemetry, getAuthLogger, getCredEnvelopeFailClosed } from './runtime.js';
|
|
53
|
+
import { atomicWrite0600, ensureStateDir, removeStateFile, writeStateFile } from './stateFiles.js';
|
|
52
54
|
/**
|
|
53
55
|
* Account name the wrapped DEK lives under (file fallback). Keeping the
|
|
54
56
|
* literal here so a future rename surfaces as a single grep target.
|
|
@@ -84,37 +86,6 @@ export function __getStateForTest() {
|
|
|
84
86
|
function wrappedDekFilePath(profile) {
|
|
85
87
|
return resolveProfileStateFile(profile, ACCOUNT_CRED_DEK_WRAPPED).path;
|
|
86
88
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Atomic 0600 write — tmp+fsync+rename+dir-fsync. Same shape as the
|
|
89
|
-
* original daemon/CLI implementations so partial-write races between
|
|
90
|
-
* `sky` and `oversky` produce the same on-disk transitions.
|
|
91
|
-
*/
|
|
92
|
-
function atomicWrite0600(target, value) {
|
|
93
|
-
const dir = path.dirname(target);
|
|
94
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
95
|
-
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
96
|
-
const fd = fs.openSync(tmp, 'w', 0o600);
|
|
97
|
-
try {
|
|
98
|
-
fs.writeSync(fd, value);
|
|
99
|
-
fs.fsyncSync(fd);
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
fs.closeSync(fd);
|
|
103
|
-
}
|
|
104
|
-
fs.renameSync(tmp, target);
|
|
105
|
-
try {
|
|
106
|
-
const dirFd = fs.openSync(dir, 'r');
|
|
107
|
-
try {
|
|
108
|
-
fs.fsyncSync(dirFd);
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
fs.closeSync(dirFd);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
/* best-effort — Windows / some BSDs reject dir fsync */
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
89
|
/**
|
|
119
90
|
* Telemetry shim. Routes through the consumer-supplied logEvent sink
|
|
120
91
|
* (daemon: structured event log; CLI: typically no-op or stderr at DEBUG).
|
|
@@ -195,9 +166,9 @@ function credEnvelopeLockPath(profile) {
|
|
|
195
166
|
async function withCredEnvelopeLock(profile, body) {
|
|
196
167
|
const lockPath = credEnvelopeLockPath(profile);
|
|
197
168
|
const dir = path.dirname(lockPath);
|
|
198
|
-
|
|
169
|
+
ensureStateDir(dir, `take the credential envelope lock for profile ${profile}`);
|
|
199
170
|
if (!fs.existsSync(lockPath)) {
|
|
200
|
-
|
|
171
|
+
writeStateFile(lockPath, '', { mode: 0o600 }, 'create the credential envelope lock');
|
|
201
172
|
}
|
|
202
173
|
const acquireStart = Date.now();
|
|
203
174
|
const release = await lockfile.lock(lockPath, {
|
|
@@ -338,6 +309,7 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
338
309
|
}));
|
|
339
310
|
}
|
|
340
311
|
catch (err) {
|
|
312
|
+
rethrowIfLiveStateRefusal(err);
|
|
341
313
|
emit('cred_envelope.path_consolidation.failed', false, {
|
|
342
314
|
profile,
|
|
343
315
|
message: err?.message ?? 'unknown',
|
|
@@ -473,11 +445,11 @@ function keepUnreadableWrappedDek(profile, wrapped) {
|
|
|
473
445
|
}
|
|
474
446
|
}
|
|
475
447
|
const name = `${UNREADABLE_DEK_COPY_PREFIX}${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
476
|
-
atomicWrite0600(path.join(dir, name), wrapped);
|
|
448
|
+
atomicWrite0600(path.join(dir, name), wrapped, 'keep the unreadable wrapped DEK');
|
|
477
449
|
const kept = [...existing, name].sort();
|
|
478
450
|
for (const stale of kept.slice(0, Math.max(0, kept.length - MAX_UNREADABLE_DEK_COPIES))) {
|
|
479
451
|
try {
|
|
480
|
-
|
|
452
|
+
removeStateFile(path.join(dir, stale), 'prune a kept wrapped DEK');
|
|
481
453
|
}
|
|
482
454
|
catch {
|
|
483
455
|
/* a copy that will not go costs 92 bytes */
|
|
@@ -486,6 +458,7 @@ function keepUnreadableWrappedDek(profile, wrapped) {
|
|
|
486
458
|
return name;
|
|
487
459
|
}
|
|
488
460
|
catch (err) {
|
|
461
|
+
rethrowIfLiveStateRefusal(err);
|
|
489
462
|
emit('cred_envelope.unreadable_dek_copy_failed', false, {
|
|
490
463
|
profile,
|
|
491
464
|
message: err?.message ?? 'unknown',
|
|
@@ -626,6 +599,9 @@ export async function initNewIndependentCredEnvelope(profile) {
|
|
|
626
599
|
const dek = generateDek();
|
|
627
600
|
let temp;
|
|
628
601
|
try {
|
|
602
|
+
// Everything below creates, links and deletes inside `dir`, with raw flags
|
|
603
|
+
// (O_EXCL, link) the shared writers do not offer, so the refusal is asked once here.
|
|
604
|
+
assertMayMutateLiveState(dir, `enroll the wrapped DEK for background profile ${profile}`);
|
|
629
605
|
const wrapped = await kek.wrap(dek, buildCredentialAad(profile));
|
|
630
606
|
// Another enrollment may have won while the platform store was busy.
|
|
631
607
|
// Its file is opened read-only; this process never overwrites it.
|
|
@@ -869,7 +845,7 @@ export async function initCredEnvelope(profile) {
|
|
|
869
845
|
try {
|
|
870
846
|
dek = generateDek();
|
|
871
847
|
const wrapped = await kek.wrap(dek, aad);
|
|
872
|
-
atomicWrite0600(targetPath, wrapped);
|
|
848
|
+
atomicWrite0600(targetPath, wrapped, `write the wrapped DEK for profile ${profile}`);
|
|
873
849
|
emit('cred_envelope.dek_enrolled', true, {
|
|
874
850
|
kekId: kek.id,
|
|
875
851
|
kekKind: captureKind(),
|
|
@@ -878,6 +854,7 @@ export async function initCredEnvelope(profile) {
|
|
|
878
854
|
});
|
|
879
855
|
}
|
|
880
856
|
catch (err) {
|
|
857
|
+
rethrowIfLiveStateRefusal(err);
|
|
881
858
|
_state = { kind: 'disabled', reason: 'init_failed' };
|
|
882
859
|
emit('cred_envelope.init_failed', false, {
|
|
883
860
|
reason: 'enrollment_failed',
|
|
@@ -926,9 +903,10 @@ export async function resetCredEnvelope(opts) {
|
|
|
926
903
|
...retiredDekFiles(opts.profile),
|
|
927
904
|
]) {
|
|
928
905
|
try {
|
|
929
|
-
|
|
906
|
+
removeStateFile(target, `delete the wrapped DEK file ${path.basename(target)}`);
|
|
930
907
|
}
|
|
931
908
|
catch (err) {
|
|
909
|
+
rethrowIfLiveStateRefusal(err);
|
|
932
910
|
if (err.code !== 'ENOENT') {
|
|
933
911
|
emit('cred_envelope.clear_failed', false, {
|
|
934
912
|
profile: opts.profile,
|
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type DeviceKeyAlg, type DevicePublicJwk } from './deviceKey.js';
|
|
2
|
+
import { type DeviceKeyCustody, type DeviceKeyHandle } from './deviceKeyHandle.js';
|
|
3
|
+
import { type HardwareKeyCapability, type HardwareKeyRecord } from './deviceKeyHardware.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `cred_envelope.read.decrypt_failed` hint for the device PRIVATE key
|
|
6
|
+
* (OSK-20506).
|
|
7
|
+
*
|
|
8
|
+
* The envelope's default hint says "signing in again rebuilds it", which is true
|
|
9
|
+
* of a credential and false of this: `skrr login` never creates or replaces a
|
|
10
|
+
* device key, and only an interactive daemon sign-in does (it re-pins the new key
|
|
11
|
+
* on the server). `loadKeypair` hands this to `maybeDecryptOnRead` so the
|
|
12
|
+
* producer says WHAT failed; a presenter that already prints one device-key line
|
|
13
|
+
* (the CLI's `recovery_required` notice) recognises it by identity and prints
|
|
14
|
+
* nothing for the decrypt failure, instead of two lines with two remedies. A
|
|
15
|
+
* logger that prints every warning, the daemon's, shows this one as written.
|
|
16
|
+
*
|
|
17
|
+
* Compared by value by the CLI presenter: change the text and the CLI prints the
|
|
18
|
+
* credential line again, which `auth-core-init.device-key.spec.ts` catches.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEVICE_KEY_UNREADABLE_CREDENTIAL_HINT = "stored device key is unreadable on this machine \u2014 only an interactive daemon sign-in replaces it";
|
|
21
|
+
/**
|
|
22
|
+
* `key` is a handle, never a keypair: nothing in this module (or any caller of
|
|
23
|
+
* it) can read a private scalar once a key is active, which is what lets a
|
|
24
|
+
* hardware key stand in for a software one without a second code path.
|
|
25
|
+
*
|
|
26
|
+
* `envelopeProtected` is about the PRIVATE KEY FILE being sealed under the
|
|
27
|
+
* credential envelope. A hardware record is deliberately not sealed (it is
|
|
28
|
+
* useless off this machine, and sealing it would tie the device identity's life
|
|
29
|
+
* to the envelope key again), so it is always `false` for hardware and says
|
|
30
|
+
* nothing about how well that key is protected: read `key.custody` for that.
|
|
31
|
+
*/
|
|
2
32
|
type DeviceIdentityState = {
|
|
3
33
|
kind: 'uninit';
|
|
4
34
|
} | {
|
|
@@ -6,7 +36,7 @@ type DeviceIdentityState = {
|
|
|
6
36
|
reason: DisabledReason;
|
|
7
37
|
} | {
|
|
8
38
|
kind: 'active';
|
|
9
|
-
|
|
39
|
+
key: DeviceKeyHandle;
|
|
10
40
|
profile: string;
|
|
11
41
|
envelopeProtected: boolean;
|
|
12
42
|
};
|
|
@@ -69,6 +99,69 @@ export declare function resetDeviceIdentity(opts?: {
|
|
|
69
99
|
profile?: string;
|
|
70
100
|
clearOnDisk?: boolean;
|
|
71
101
|
}): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* A hardware key that exists only in memory. It is not this machine's identity
|
|
104
|
+
* until `commitDeviceKeyCandidate`, and it leaves nothing behind if it is
|
|
105
|
+
* dropped: a Secure Enclave key lives in the blob and nowhere else.
|
|
106
|
+
*
|
|
107
|
+
* That is the whole safety argument for an upgrade. The server learns the new key
|
|
108
|
+
* from a proof signed with it (an interactive sign-in re-pins the row), so the
|
|
109
|
+
* order has to be: create the candidate, sign the sign-in with it, and only after
|
|
110
|
+
* the server has accepted that sign-in make it the identity. The software key
|
|
111
|
+
* stays on disk, untouched and active, through every step before the last one.
|
|
112
|
+
*/
|
|
113
|
+
export interface DeviceKeyCandidate {
|
|
114
|
+
readonly key: DeviceKeyHandle;
|
|
115
|
+
/** @internal what `commitDeviceKeyCandidate` persists. */
|
|
116
|
+
readonly record: HardwareKeyRecord;
|
|
117
|
+
}
|
|
118
|
+
export type DeviceKeyCandidateResult = {
|
|
119
|
+
ok: true;
|
|
120
|
+
candidate: DeviceKeyCandidate;
|
|
121
|
+
} | {
|
|
122
|
+
ok: false;
|
|
123
|
+
reason: 'already_hardware' | 'independent_profile' | 'unavailable' | 'create_failed';
|
|
124
|
+
capability?: HardwareKeyCapability;
|
|
125
|
+
message?: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Create a hardware key candidate if this machine can hold one and does not
|
|
129
|
+
* already. Never throws and never touches disk, so an interactive sign-in can
|
|
130
|
+
* call it unconditionally and fall back to the key it has on any answer but ok.
|
|
131
|
+
*/
|
|
132
|
+
export declare function prepareHardwareKeyCandidate(profile: string): Promise<DeviceKeyCandidateResult>;
|
|
133
|
+
/** Sign a request with a candidate (the upgrade's sign-in proof). Null on failure, like `signServerRequest`. */
|
|
134
|
+
export declare function signWithCandidate(candidate: DeviceKeyCandidate, opts: {
|
|
135
|
+
method: string;
|
|
136
|
+
url: string;
|
|
137
|
+
refreshToken?: string;
|
|
138
|
+
accessToken?: string;
|
|
139
|
+
}): Promise<string | null>;
|
|
140
|
+
/**
|
|
141
|
+
* Make a candidate this profile's device identity. Call it only after the
|
|
142
|
+
* server has accepted a sign-in proven with `candidate`: until then the software
|
|
143
|
+
* key is the identity the server pins, and replacing it early is the failure
|
|
144
|
+
* this API exists to prevent.
|
|
145
|
+
*
|
|
146
|
+
* Under the identity lock, in the order that survives a crash: retire the old
|
|
147
|
+
* key, write the hardware record (authoritative), then the public file, then
|
|
148
|
+
* clear the enrollment marker (it records the OLD thumbprint and would otherwise
|
|
149
|
+
* short-circuit every later enrollment as already done).
|
|
150
|
+
*/
|
|
151
|
+
export declare function commitDeviceKeyCandidate(profile: string, candidate: DeviceKeyCandidate): Promise<void>;
|
|
152
|
+
/** A sign-in that takes longer than this has been abandoned; its pending key is not committed. */
|
|
153
|
+
export declare const PENDING_HARDWARE_KEY_MAX_AGE_MS: number;
|
|
154
|
+
/** Park a candidate so a later process can commit or discard it. Replaces any earlier one. */
|
|
155
|
+
export declare function persistPendingHardwareKey(profile: string, candidate: DeviceKeyCandidate): Promise<void>;
|
|
156
|
+
export type PendingHardwareKeyOutcome = 'committed' | 'no_pending' | 'mismatch' | 'expired' | 'unreadable';
|
|
157
|
+
/**
|
|
158
|
+
* Make the parked candidate this profile's identity, if it is the one the caller
|
|
159
|
+
* proved the sign-in with (`thumbprint`) and it is not stale. Anything else
|
|
160
|
+
* commits nothing: the software key stays the identity, and the answer says why.
|
|
161
|
+
*/
|
|
162
|
+
export declare function commitPendingHardwareKey(profile: string, thumbprint: string): Promise<PendingHardwareKeyOutcome>;
|
|
163
|
+
/** Drop the parked candidate (the sign-in failed or was refused). Idempotent. */
|
|
164
|
+
export declare function discardPendingHardwareKey(profile: string): Promise<void>;
|
|
72
165
|
/** True iff init has completed and a keypair is in memory. */
|
|
73
166
|
export declare function isDeviceIdentityActive(): boolean;
|
|
74
167
|
/**
|
|
@@ -77,8 +170,10 @@ export declare function isDeviceIdentityActive(): boolean;
|
|
|
77
170
|
* on the in-flight request.
|
|
78
171
|
*/
|
|
79
172
|
export declare function getDevicePublicKey(): {
|
|
80
|
-
jwk:
|
|
173
|
+
jwk: DevicePublicJwk;
|
|
81
174
|
thumbprint: string;
|
|
175
|
+
alg: DeviceKeyAlg;
|
|
176
|
+
custody: DeviceKeyCustody;
|
|
82
177
|
} | null;
|
|
83
178
|
/**
|
|
84
179
|
* Operator-readable summary of device-identity state. Used by
|
|
@@ -90,13 +185,47 @@ export declare function describeDeviceIdentityState(): {
|
|
|
90
185
|
thumbprint?: string;
|
|
91
186
|
profile?: string;
|
|
92
187
|
envelopeProtected?: boolean;
|
|
188
|
+
alg?: DeviceKeyAlg;
|
|
189
|
+
/** Whether the private half can be copied off this machine. Absent unless active. */
|
|
190
|
+
custody?: DeviceKeyCustody;
|
|
191
|
+
/**
|
|
192
|
+
* For a hardware key: can THIS process sign with it right now? False means the
|
|
193
|
+
* key exists and is intact but the helper that signs with it is not installed
|
|
194
|
+
* where this process can find it (a CLI without the desktop app's helper, an
|
|
195
|
+
* app update that has not landed). It is a different fact from an unreadable
|
|
196
|
+
* key, and it must never lead to replacing the key.
|
|
197
|
+
*/
|
|
198
|
+
canSign?: boolean;
|
|
199
|
+
};
|
|
200
|
+
export type StoredDeviceKeyDescription = {
|
|
201
|
+
stored: false;
|
|
202
|
+
} | {
|
|
203
|
+
stored: true;
|
|
204
|
+
alg: DeviceKeyAlg;
|
|
205
|
+
custody: DeviceKeyCustody;
|
|
206
|
+
thumbprint: string;
|
|
207
|
+
/** A hardware key needs the helper to sign; a software key never does. */
|
|
208
|
+
canSign: boolean;
|
|
93
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* What a profile's device key IS, read from its plaintext public record alone. For
|
|
212
|
+
* `status`, which is a different process from the daemon and may run while the
|
|
213
|
+
* daemon is stopped: it opens nothing, needs no envelope key and writes nothing, so
|
|
214
|
+
* it answers even when the private half is unreadable. Not evidence the key can be
|
|
215
|
+
* used (only `describeDeviceIdentityState` in the process that loaded it says that);
|
|
216
|
+
* evidence of what kind of key the profile has, which nothing else showed.
|
|
217
|
+
*/
|
|
218
|
+
export declare function describeStoredDeviceKey(profile: string): StoredDeviceKeyDescription;
|
|
94
219
|
/**
|
|
95
220
|
* Sign an outgoing request with the device key. Returns the JWS Compact
|
|
96
221
|
* Serialization, or null when device identity is not active. NEVER
|
|
97
222
|
* throws — a failed sign returns null and emits a telemetry event so
|
|
98
223
|
* the caller can fall back to bearer-only auth without an outage.
|
|
99
224
|
*
|
|
225
|
+
* Asynchronous because a hardware key signs in a helper process. A software key
|
|
226
|
+
* resolves on the next microtask, so a caller that awaits pays nothing it can
|
|
227
|
+
* measure; a caller that could not await could not have a hardware key.
|
|
228
|
+
*
|
|
100
229
|
* Shape matches `DeviceProofSigner` from `./refresh.ts` so the function
|
|
101
230
|
* passes directly into `refreshDaemonToken`'s `signRequestProof` slot.
|
|
102
231
|
*/
|
|
@@ -105,7 +234,7 @@ export declare function signServerRequest(opts: {
|
|
|
105
234
|
url: string;
|
|
106
235
|
refreshToken?: string;
|
|
107
236
|
accessToken?: string;
|
|
108
|
-
}): string | null
|
|
237
|
+
}): Promise<string | null>;
|
|
109
238
|
/** @internal Test seams — file path resolvers. */
|
|
110
239
|
export declare function __publicKeyPathForTest(profile: string): string;
|
|
111
240
|
export declare function __privateKeyPathForTest(profile: string): string;
|