@skrr-ai/cli 0.1.120 → 0.1.121
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.js +11 -2
- package/dist/commands/agent/message/context.d.ts +1 -0
- package/dist/commands/agent/message/context.js +3 -2
- package/dist/commands/search/agents.js +5 -1
- package/dist/commands/search/all.js +74 -22
- package/dist/commands/search/skills.d.ts +1 -0
- package/dist/commands/search/skills.js +12 -1
- package/dist/commands/search/spaces.d.ts +8 -1
- package/dist/commands/search/spaces.js +50 -12
- package/dist/commands/search/tasks.d.ts +13 -2
- package/dist/commands/search/tasks.js +78 -14
- package/dist/commands/search/teams.js +5 -1
- package/dist/commands/search/workspace.d.ts +19 -0
- package/dist/commands/search/workspace.js +126 -0
- package/dist/commands/skills/browse.d.ts +1 -0
- package/dist/commands/skills/browse.js +8 -1
- package/dist/commands/skills/search.d.ts +1 -0
- package/dist/commands/skills/search.js +12 -1
- package/dist/commands/spaces/list.d.ts +1 -0
- package/dist/commands/spaces/list.js +12 -3
- package/dist/commands/spaces/search.d.ts +1 -0
- package/dist/commands/spaces/search.js +9 -0
- package/dist/commands/tasks/search.d.ts +1 -0
- package/dist/commands/tasks/search.js +9 -0
- package/dist/lib/api-error-reason.d.ts +2 -0
- package/dist/lib/api-error-reason.js +14 -0
- package/dist/lib/api-fetch.js +3 -0
- package/dist/lib/search-display.d.ts +7 -0
- package/dist/lib/search-display.js +31 -0
- package/dist/lib/skill-store-pagination.d.ts +8 -0
- package/dist/lib/skill-store-pagination.js +35 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonConsentProof.d.ts +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonConsentProof.js +72 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopDaemonProof.d.ts +21 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopDaemonProof.js +96 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +2 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +102 -20
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.d.ts +27 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +161 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineScopes.d.ts +26 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineScopes.js +41 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +20 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +172 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonConsentProof.d.ts +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonConsentProof.js +43 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopDaemonProof.d.ts +21 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopDaemonProof.js +62 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +2 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +133 -25
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.d.ts +27 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +132 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineScopes.d.ts +26 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineScopes.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +28 -15
- package/dist/node_modules/@skrr-ai/auth-core/package.json +31 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7627 -7574
- package/oclif.manifest.json +634 -21
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
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 independentMachineReservation_exports = {};
|
|
30
|
+
__export(independentMachineReservation_exports, {
|
|
31
|
+
INDEPENDENT_MACHINE_PROFILE_RE: () => INDEPENDENT_MACHINE_PROFILE_RE,
|
|
32
|
+
INDEPENDENT_MACHINE_RESERVATION_FILE: () => INDEPENDENT_MACHINE_RESERVATION_FILE,
|
|
33
|
+
readIndependentMachineReservation: () => readIndependentMachineReservation,
|
|
34
|
+
reserveIndependentMachineProfile: () => reserveIndependentMachineProfile,
|
|
35
|
+
verifyIndependentMachineReservation: () => verifyIndependentMachineReservation
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(independentMachineReservation_exports);
|
|
38
|
+
var import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
39
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
40
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
41
|
+
const INDEPENDENT_MACHINE_PROFILE_RE = /^desktop-bg-[a-f0-9]{32}$/;
|
|
42
|
+
const INDEPENDENT_MACHINE_RESERVATION_FILE = "independent-machine-attempt.json";
|
|
43
|
+
function canonicalOrigin(input) {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(input);
|
|
46
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
47
|
+
return !url.username && !url.password && url.origin === input && (url.protocol === "https:" || url.protocol === "http:" && local);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function validSecret(secret) {
|
|
53
|
+
return typeof secret === "string" && /^[A-Za-z0-9_-]{43}$/.test(secret) && Buffer.from(secret, "base64url").length === 32 && Buffer.from(secret, "base64url").toString("base64url") === secret;
|
|
54
|
+
}
|
|
55
|
+
function hashSecret(profile, ownerId, origin, secret) {
|
|
56
|
+
return import_node_crypto.default.createHash("sha256").update("independent-machine-reservation:v2\0").update(profile).update("\0").update(ownerId).update("\0").update(origin).update("\0").update(secret).digest();
|
|
57
|
+
}
|
|
58
|
+
function reservationPath(root, profile) {
|
|
59
|
+
if (!import_node_path.default.isAbsolute(root) || !INDEPENDENT_MACHINE_PROFILE_RE.test(profile)) {
|
|
60
|
+
throw new Error("Invalid independent machine profile");
|
|
61
|
+
}
|
|
62
|
+
return import_node_path.default.join(root, "profiles", profile, INDEPENDENT_MACHINE_RESERVATION_FILE);
|
|
63
|
+
}
|
|
64
|
+
function reserveIndependentMachineProfile(options) {
|
|
65
|
+
if (!import_node_path.default.isAbsolute(options.root) || !/^[a-fA-F0-9]{24}$/.test(options.ownerId) || !canonicalOrigin(options.origin))
|
|
66
|
+
throw new Error("Invalid background authorization context");
|
|
67
|
+
const secret = import_node_crypto.default.randomBytes(32).toString("base64url");
|
|
68
|
+
const encrypted = options.encrypt(secret);
|
|
69
|
+
if (!Buffer.isBuffer(encrypted) || encrypted.length === 0 || encrypted.length > 4096) {
|
|
70
|
+
throw new Error("Background authorization secure storage is unavailable");
|
|
71
|
+
}
|
|
72
|
+
const profiles = import_node_path.default.join(options.root, "profiles");
|
|
73
|
+
const rootExisted = import_node_fs.default.existsSync(options.root);
|
|
74
|
+
const profilesExisted = import_node_fs.default.existsSync(profiles);
|
|
75
|
+
import_node_fs.default.mkdirSync(profiles, { recursive: true, mode: 448 });
|
|
76
|
+
for (let i = 0; i < 5; i += 1) {
|
|
77
|
+
const profile = `desktop-bg-${import_node_crypto.default.randomBytes(16).toString("hex")}`;
|
|
78
|
+
const dir = import_node_path.default.join(profiles, profile);
|
|
79
|
+
try {
|
|
80
|
+
import_node_fs.default.mkdirSync(dir, { mode: 448 });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === "EEXIST")
|
|
83
|
+
continue;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const reservation = {
|
|
87
|
+
version: 2,
|
|
88
|
+
profile,
|
|
89
|
+
ownerId: options.ownerId.toLowerCase(),
|
|
90
|
+
origin: options.origin,
|
|
91
|
+
secretHash: hashSecret(profile, options.ownerId.toLowerCase(), options.origin, secret).toString("hex"),
|
|
92
|
+
encryptedSecret: encrypted.toString("base64"),
|
|
93
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
94
|
+
};
|
|
95
|
+
const file = reservationPath(options.root, profile);
|
|
96
|
+
const fd = import_node_fs.default.openSync(file, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL | (import_node_fs.default.constants.O_NOFOLLOW ?? 0), 384);
|
|
97
|
+
try {
|
|
98
|
+
import_node_fs.default.writeFileSync(fd, `${JSON.stringify(reservation)}
|
|
99
|
+
`, "utf8");
|
|
100
|
+
import_node_fs.default.fsyncSync(fd);
|
|
101
|
+
} finally {
|
|
102
|
+
import_node_fs.default.closeSync(fd);
|
|
103
|
+
}
|
|
104
|
+
const directories = [
|
|
105
|
+
dir,
|
|
106
|
+
profiles,
|
|
107
|
+
...!profilesExisted ? [options.root] : [],
|
|
108
|
+
...!rootExisted ? [import_node_path.default.dirname(options.root)] : []
|
|
109
|
+
];
|
|
110
|
+
for (const directory of directories) {
|
|
111
|
+
const directoryFd = import_node_fs.default.openSync(directory, "r");
|
|
112
|
+
try {
|
|
113
|
+
import_node_fs.default.fsyncSync(directoryFd);
|
|
114
|
+
} finally {
|
|
115
|
+
import_node_fs.default.closeSync(directoryFd);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { reservation, secret };
|
|
119
|
+
}
|
|
120
|
+
throw new Error("Could not reserve a new background machine profile");
|
|
121
|
+
}
|
|
122
|
+
function readIndependentMachineReservation(root, profile) {
|
|
123
|
+
try {
|
|
124
|
+
const file = reservationPath(root, profile);
|
|
125
|
+
const directory = import_node_fs.default.lstatSync(import_node_path.default.dirname(file));
|
|
126
|
+
if (!directory.isDirectory() || directory.isSymbolicLink())
|
|
127
|
+
return null;
|
|
128
|
+
const fd = import_node_fs.default.openSync(file, import_node_fs.default.constants.O_RDONLY | (import_node_fs.default.constants.O_NOFOLLOW ?? 0));
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
const stat = import_node_fs.default.fstatSync(fd);
|
|
132
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 8192 || (stat.mode & 63) !== 0)
|
|
133
|
+
return null;
|
|
134
|
+
raw = import_node_fs.default.readFileSync(fd, "utf8");
|
|
135
|
+
} finally {
|
|
136
|
+
import_node_fs.default.closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
const value = JSON.parse(raw);
|
|
139
|
+
if (value.version !== 2 || value.profile !== profile || typeof value.ownerId !== "string" || !/^[a-f0-9]{24}$/.test(value.ownerId) || typeof value.origin !== "string" || !canonicalOrigin(value.origin) || typeof value.secretHash !== "string" || !/^[a-f0-9]{64}$/.test(value.secretHash) || typeof value.encryptedSecret !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(value.encryptedSecret) || typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt)))
|
|
140
|
+
return null;
|
|
141
|
+
return value;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function verifyIndependentMachineReservation(root, profile, secret) {
|
|
147
|
+
if (!validSecret(secret))
|
|
148
|
+
return false;
|
|
149
|
+
const saved = readIndependentMachineReservation(root, profile);
|
|
150
|
+
if (!saved)
|
|
151
|
+
return false;
|
|
152
|
+
return import_node_crypto.default.timingSafeEqual(Buffer.from(saved.secretHash, "hex"), hashSecret(profile, saved.ownerId, saved.origin, secret));
|
|
153
|
+
}
|
|
154
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
155
|
+
0 && (module.exports = {
|
|
156
|
+
INDEPENDENT_MACHINE_PROFILE_RE,
|
|
157
|
+
INDEPENDENT_MACHINE_RESERVATION_FILE,
|
|
158
|
+
readIndependentMachineReservation,
|
|
159
|
+
reserveIndependentMachineProfile,
|
|
160
|
+
verifyIndependentMachineReservation
|
|
161
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capabilities explained to a person before a separate background
|
|
3
|
+
* machine is authorized. This version is an immutable wire contract: adding
|
|
4
|
+
* access needs a new contract and a new authorization, not an edit to v1.
|
|
5
|
+
*/
|
|
6
|
+
export declare const INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1: "desktop-background-v1";
|
|
7
|
+
export declare const INDEPENDENT_MACHINE_PERMISSIONS_V1: readonly [Readonly<{
|
|
8
|
+
scope: "daemon:tools:bash";
|
|
9
|
+
description: "Run terminal commands and local agents";
|
|
10
|
+
}>, Readonly<{
|
|
11
|
+
scope: "daemon:tools:fs:read";
|
|
12
|
+
description: "Read files in allowed folders";
|
|
13
|
+
}>, Readonly<{
|
|
14
|
+
scope: "daemon:tools:fs:write";
|
|
15
|
+
description: "Create and edit files in allowed folders";
|
|
16
|
+
}>, Readonly<{
|
|
17
|
+
scope: "daemon:browser:cdp";
|
|
18
|
+
description: "Control connected browser tabs";
|
|
19
|
+
}>, Readonly<{
|
|
20
|
+
scope: "daemon:session:replay";
|
|
21
|
+
description: "Replay results recorded while offline";
|
|
22
|
+
}>, Readonly<{
|
|
23
|
+
scope: "daemon:offline-queue";
|
|
24
|
+
description: "Send queued results after reconnecting";
|
|
25
|
+
}>];
|
|
26
|
+
export declare const INDEPENDENT_MACHINE_SCOPES_V1: readonly string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var independentMachineScopes_exports = {};
|
|
20
|
+
__export(independentMachineScopes_exports, {
|
|
21
|
+
INDEPENDENT_MACHINE_PERMISSIONS_V1: () => INDEPENDENT_MACHINE_PERMISSIONS_V1,
|
|
22
|
+
INDEPENDENT_MACHINE_SCOPES_V1: () => INDEPENDENT_MACHINE_SCOPES_V1,
|
|
23
|
+
INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1: () => INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(independentMachineScopes_exports);
|
|
26
|
+
const INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1 = "desktop-background-v1";
|
|
27
|
+
const INDEPENDENT_MACHINE_PERMISSIONS_V1 = Object.freeze([
|
|
28
|
+
Object.freeze({ scope: "daemon:tools:bash", description: "Run terminal commands and local agents" }),
|
|
29
|
+
Object.freeze({ scope: "daemon:tools:fs:read", description: "Read files in allowed folders" }),
|
|
30
|
+
Object.freeze({ scope: "daemon:tools:fs:write", description: "Create and edit files in allowed folders" }),
|
|
31
|
+
Object.freeze({ scope: "daemon:browser:cdp", description: "Control connected browser tabs" }),
|
|
32
|
+
Object.freeze({ scope: "daemon:session:replay", description: "Replay results recorded while offline" }),
|
|
33
|
+
Object.freeze({ scope: "daemon:offline-queue", description: "Send queued results after reconnecting" })
|
|
34
|
+
]);
|
|
35
|
+
const INDEPENDENT_MACHINE_SCOPES_V1 = Object.freeze(INDEPENDENT_MACHINE_PERMISSIONS_V1.map((permission) => permission.scope));
|
|
36
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
37
|
+
0 && (module.exports = {
|
|
38
|
+
INDEPENDENT_MACHINE_PERMISSIONS_V1,
|
|
39
|
+
INDEPENDENT_MACHINE_SCOPES_V1,
|
|
40
|
+
INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1
|
|
41
|
+
});
|
|
@@ -28,7 +28,7 @@ export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as cred
|
|
|
28
28
|
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
|
|
29
29
|
export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
|
|
30
30
|
export { DPOP_HEADER, DPOP_MAX_AGE_SECONDS, DPOP_PROOF_INVALID, DPOP_PROOF_REQUIRED, DPOP_PROOF_TYP, DPOP_PROOF_VERIFY_UNAVAILABLE, accessTokenHash, createDpopKey, isAth, isJwkThumbprint, normalizeDpopHtu, verifyDpopProof, type DpopKey, type DpopProofClaims, type DpopProofRequest, type DpopVerifyFailureReason, type DpopVerifyOptions, type DpopVerifyResult, } from './dpop.js';
|
|
31
|
-
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
31
|
+
export { initCredEnvelope, initExistingCredEnvelope, initNewIndependentCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
32
32
|
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
33
33
|
export { getMachineUuid, getMachineUuidForProfile, describeMachineIdentity, type MachineIdentityDescription, type MachineIdentityReason, getHardwareMachineUuid, setMachineUuidProfile, getMachineUuidProfile, qualifyMachineUuid, MACHINE_IDENTITY_FILENAME, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
34
34
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
@@ -50,3 +50,4 @@ export * from './harnessPermissionControls.js';
|
|
|
50
50
|
export { compareReleaseVersions } from './releaseVersions.js';
|
|
51
51
|
export * from './computerUse.js';
|
|
52
52
|
export * from './desktopWire.js';
|
|
53
|
+
export { INDEPENDENT_MACHINE_PROFILE_RE, INDEPENDENT_MACHINE_RESERVATION_FILE, readIndependentMachineReservation, reserveIndependentMachineProfile, verifyIndependentMachineReservation, type IndependentMachineReservation, } from './independentMachineReservation.js';
|
|
@@ -47,6 +47,8 @@ __export(index_exports, {
|
|
|
47
47
|
HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT: () => import_harnessSkillLayout.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT,
|
|
48
48
|
HARNESS_TIERS: () => import_harnessTrust.HARNESS_TIERS,
|
|
49
49
|
HOSTED_RUNTIME_MACHINE_SOURCES: () => import_machineUuid.HOSTED_RUNTIME_MACHINE_SOURCES,
|
|
50
|
+
INDEPENDENT_MACHINE_PROFILE_RE: () => import_independentMachineReservation.INDEPENDENT_MACHINE_PROFILE_RE,
|
|
51
|
+
INDEPENDENT_MACHINE_RESERVATION_FILE: () => import_independentMachineReservation.INDEPENDENT_MACHINE_RESERVATION_FILE,
|
|
50
52
|
InMemoryKek: () => import_kek.InMemoryKek,
|
|
51
53
|
KekUnavailableError: () => import_kek.KekUnavailableError,
|
|
52
54
|
LEGACY_CONFIG_DIR_NAMES: () => import_localIdentity.LEGACY_CONFIG_DIR_NAMES,
|
|
@@ -189,6 +191,8 @@ __export(index_exports, {
|
|
|
189
191
|
hostedRuntimeIdOverride: () => import_machineUuid.hostedRuntimeIdOverride,
|
|
190
192
|
initCredEnvelope: () => import_credentialEnvelopeBridge.initCredEnvelope,
|
|
191
193
|
initDeviceIdentity: () => import_deviceIdentityBridge.initDeviceIdentity,
|
|
194
|
+
initExistingCredEnvelope: () => import_credentialEnvelopeBridge.initExistingCredEnvelope,
|
|
195
|
+
initNewIndependentCredEnvelope: () => import_credentialEnvelopeBridge.initNewIndependentCredEnvelope,
|
|
192
196
|
installLockExitHandlers: () => import_refresh.installLockExitHandlers,
|
|
193
197
|
invalidateAuthHelperCache: () => import_credentialResolver.invalidateAuthHelperCache,
|
|
194
198
|
invalidateHeadersHelperCache: () => import_headers_helper.invalidateHeadersHelperCache,
|
|
@@ -238,9 +242,11 @@ __export(index_exports, {
|
|
|
238
242
|
publicJwkFromX: () => import_deviceKey.publicJwkFromX,
|
|
239
243
|
qualifyMachineUuid: () => import_machineUuid.qualifyMachineUuid,
|
|
240
244
|
readAuthFromFd: () => import_fdToken.readAuthFromFd,
|
|
245
|
+
readIndependentMachineReservation: () => import_independentMachineReservation.readIndependentMachineReservation,
|
|
241
246
|
reauthCommand: () => import_messages.reauthCommand,
|
|
242
247
|
refreshDaemonToken: () => import_refresh.refreshDaemonToken,
|
|
243
248
|
registerZeroizeHook: () => import_kek.registerZeroizeHook,
|
|
249
|
+
reserveIndependentMachineProfile: () => import_independentMachineReservation.reserveIndependentMachineProfile,
|
|
244
250
|
resetCredEnvelope: () => import_credentialEnvelopeBridge.resetCredEnvelope,
|
|
245
251
|
resetDeviceIdentity: () => import_deviceIdentityBridge.resetDeviceIdentity,
|
|
246
252
|
resolveChannelManifestUrl: () => import_firstPartyHarnessChannels.resolveChannelManifestUrl,
|
|
@@ -263,6 +269,7 @@ __export(index_exports, {
|
|
|
263
269
|
verifyChallenge: () => import_pkce.verifyChallenge,
|
|
264
270
|
verifyDeviceProof: () => import_deviceKey.verifyProof,
|
|
265
271
|
verifyDpopProof: () => import_dpop.verifyDpopProof,
|
|
272
|
+
verifyIndependentMachineReservation: () => import_independentMachineReservation.verifyIndependentMachineReservation,
|
|
266
273
|
verifyManifest: () => import_releaseManifest.verifyManifest,
|
|
267
274
|
withAuthLock: () => import_refresh.withAuthLock,
|
|
268
275
|
withLoopbackNoProxy: () => import_spawnEnv.withLoopbackNoProxy,
|
|
@@ -313,6 +320,7 @@ __reExport(index_exports, require("./harnessPermissionControls.js"), module.expo
|
|
|
313
320
|
var import_releaseVersions = require("./releaseVersions.js");
|
|
314
321
|
__reExport(index_exports, require("./computerUse.js"), module.exports);
|
|
315
322
|
__reExport(index_exports, require("./desktopWire.js"), module.exports);
|
|
323
|
+
var import_independentMachineReservation = require("./independentMachineReservation.js");
|
|
316
324
|
// Annotate the CommonJS export names for ESM import in node:
|
|
317
325
|
0 && (module.exports = {
|
|
318
326
|
ALL_DAEMON_SCOPES,
|
|
@@ -343,6 +351,8 @@ __reExport(index_exports, require("./desktopWire.js"), module.exports);
|
|
|
343
351
|
HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT,
|
|
344
352
|
HARNESS_TIERS,
|
|
345
353
|
HOSTED_RUNTIME_MACHINE_SOURCES,
|
|
354
|
+
INDEPENDENT_MACHINE_PROFILE_RE,
|
|
355
|
+
INDEPENDENT_MACHINE_RESERVATION_FILE,
|
|
346
356
|
InMemoryKek,
|
|
347
357
|
KekUnavailableError,
|
|
348
358
|
LEGACY_CONFIG_DIR_NAMES,
|
|
@@ -485,6 +495,8 @@ __reExport(index_exports, require("./desktopWire.js"), module.exports);
|
|
|
485
495
|
hostedRuntimeIdOverride,
|
|
486
496
|
initCredEnvelope,
|
|
487
497
|
initDeviceIdentity,
|
|
498
|
+
initExistingCredEnvelope,
|
|
499
|
+
initNewIndependentCredEnvelope,
|
|
488
500
|
installLockExitHandlers,
|
|
489
501
|
invalidateAuthHelperCache,
|
|
490
502
|
invalidateHeadersHelperCache,
|
|
@@ -534,9 +546,11 @@ __reExport(index_exports, require("./desktopWire.js"), module.exports);
|
|
|
534
546
|
publicJwkFromX,
|
|
535
547
|
qualifyMachineUuid,
|
|
536
548
|
readAuthFromFd,
|
|
549
|
+
readIndependentMachineReservation,
|
|
537
550
|
reauthCommand,
|
|
538
551
|
refreshDaemonToken,
|
|
539
552
|
registerZeroizeHook,
|
|
553
|
+
reserveIndependentMachineProfile,
|
|
540
554
|
resetCredEnvelope,
|
|
541
555
|
resetDeviceIdentity,
|
|
542
556
|
resolveChannelManifestUrl,
|
|
@@ -559,6 +573,7 @@ __reExport(index_exports, require("./desktopWire.js"), module.exports);
|
|
|
559
573
|
verifyChallenge,
|
|
560
574
|
verifyDeviceProof,
|
|
561
575
|
verifyDpopProof,
|
|
576
|
+
verifyIndependentMachineReservation,
|
|
562
577
|
verifyManifest,
|
|
563
578
|
withAuthLock,
|
|
564
579
|
withLoopbackNoProxy,
|
|
@@ -178,7 +178,6 @@ async function writeKekToKeychain(kek, service, account) {
|
|
|
178
178
|
account,
|
|
179
179
|
"-w",
|
|
180
180
|
b64,
|
|
181
|
-
"-U",
|
|
182
181
|
keychainPath
|
|
183
182
|
]);
|
|
184
183
|
} catch (err) {
|
|
@@ -219,9 +218,26 @@ async function resolveOrCreateKek(service, account) {
|
|
|
219
218
|
return existing;
|
|
220
219
|
}
|
|
221
220
|
const fresh = import_node_crypto.default.randomBytes(import_credentialEnvelope.KEY_LEN);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
try {
|
|
222
|
+
try {
|
|
223
|
+
await writeKekToKeychain(fresh, service, account);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const winner = await readKekFromKeychain(service, account).catch(() => null);
|
|
226
|
+
if (!winner)
|
|
227
|
+
throw error;
|
|
228
|
+
_kekCacheBySvc.set(cacheKey, winner);
|
|
229
|
+
return winner;
|
|
230
|
+
}
|
|
231
|
+
const persisted = await readKekFromKeychain(service, account);
|
|
232
|
+
if (!persisted || !persisted.equals(fresh)) {
|
|
233
|
+
persisted?.fill(0);
|
|
234
|
+
throw new import_types.KekUnavailableError(KIND_SHELLOUT, "created master KEK could not be verified");
|
|
235
|
+
}
|
|
236
|
+
_kekCacheBySvc.set(cacheKey, persisted);
|
|
237
|
+
return persisted;
|
|
238
|
+
} finally {
|
|
239
|
+
fresh.fill(0);
|
|
240
|
+
}
|
|
225
241
|
}
|
|
226
242
|
async function resolveExistingKek(service, account) {
|
|
227
243
|
const cacheKey = `${service}::${account}`;
|
|
@@ -54,6 +54,16 @@ export declare function splitKeyConvergencePolicy(): {
|
|
|
54
54
|
converge: boolean;
|
|
55
55
|
reason: 'daemon_key_pinned' | 'daemon_key_refused';
|
|
56
56
|
};
|
|
57
|
+
/** Open only the existing wrapped DEK. Recovery must never enroll a new key,
|
|
58
|
+
* consolidate profile paths or replace an unreadable envelope. A caller gets
|
|
59
|
+
* an error if the existing file or the platform's read-only unwrap is unsafe
|
|
60
|
+
* or unavailable. This intentionally does not load retired keys; credentials
|
|
61
|
+
* that require one remain unavailable rather than triggering a migration. */
|
|
62
|
+
export declare function initExistingCredEnvelope(profile: string): Promise<void>;
|
|
63
|
+
/** First enrollment for a reserved desktop background profile. Existing or
|
|
64
|
+
* ambiguous state is opened read-only or refused; it is never re-enrolled.
|
|
65
|
+
* The caller must validate the reservation before calling this function. */
|
|
66
|
+
export declare function initNewIndependentCredEnvelope(profile: string): Promise<void>;
|
|
57
67
|
/**
|
|
58
68
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
59
69
|
* non-uninit returns immediately.
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* when not active. CLI leaves it unset; legacy `OVERSKY_KEK_REQUIRED=1`
|
|
41
41
|
* env var is still honored as a back-compat trigger.
|
|
42
42
|
*/
|
|
43
|
-
import { createHash } from 'node:crypto';
|
|
43
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
44
44
|
import fs from 'node:fs';
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
import lockfile from 'proper-lockfile';
|
|
@@ -429,6 +429,172 @@ function retiredDekFiles(profile) {
|
|
|
429
429
|
return [];
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
|
+
/** Open only the existing wrapped DEK. Recovery must never enroll a new key,
|
|
433
|
+
* consolidate profile paths or replace an unreadable envelope. A caller gets
|
|
434
|
+
* an error if the existing file or the platform's read-only unwrap is unsafe
|
|
435
|
+
* or unavailable. This intentionally does not load retired keys; credentials
|
|
436
|
+
* that require one remain unavailable rather than triggering a migration. */
|
|
437
|
+
export async function initExistingCredEnvelope(profile) {
|
|
438
|
+
if (_state.kind === 'active' && _state.profile === profile)
|
|
439
|
+
return;
|
|
440
|
+
if (_state.kind !== 'uninit' || typeof profile !== 'string' || !profile) {
|
|
441
|
+
throw new Error('Existing credential envelope is unavailable');
|
|
442
|
+
}
|
|
443
|
+
let fd;
|
|
444
|
+
let candidate;
|
|
445
|
+
try {
|
|
446
|
+
const target = wrappedDekFilePath(profile);
|
|
447
|
+
fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
448
|
+
const before = fs.fstatSync(fd);
|
|
449
|
+
const safe = (stat) => stat.isFile() && stat.nlink === 1 && stat.size > 0 && stat.size <= 65_536 &&
|
|
450
|
+
(stat.mode & 0o077) === 0 && (typeof process.getuid !== 'function' || stat.uid === process.getuid());
|
|
451
|
+
if (!safe(before))
|
|
452
|
+
throw new Error('unsafe envelope');
|
|
453
|
+
const readBounded = () => {
|
|
454
|
+
const bytes = Buffer.alloc(before.size);
|
|
455
|
+
let offset = 0;
|
|
456
|
+
while (offset < bytes.length) {
|
|
457
|
+
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
458
|
+
if (count <= 0)
|
|
459
|
+
throw new Error('changed envelope');
|
|
460
|
+
offset += count;
|
|
461
|
+
}
|
|
462
|
+
return bytes;
|
|
463
|
+
};
|
|
464
|
+
const wrapped = readBounded();
|
|
465
|
+
const kek = getKekStrategy();
|
|
466
|
+
if (!kek.unwrapExisting || !(await kek.available()))
|
|
467
|
+
throw new Error('read-only KEK unavailable');
|
|
468
|
+
candidate = await kek.unwrapExisting(wrapped, buildCredentialAad(profile));
|
|
469
|
+
if (!Buffer.isBuffer(candidate) || candidate.length !== 32)
|
|
470
|
+
throw new Error('invalid envelope');
|
|
471
|
+
// The unwrap can wait for Keychain. A different file, a replaced path or
|
|
472
|
+
// a second initializer must not make this process adopt a stale key.
|
|
473
|
+
if (!readBounded().equals(wrapped))
|
|
474
|
+
throw new Error('changed envelope');
|
|
475
|
+
const after = fs.fstatSync(fd);
|
|
476
|
+
const current = fs.lstatSync(target);
|
|
477
|
+
if (!safe(after) || !safe(current) || before.dev !== after.dev || before.ino !== after.ino ||
|
|
478
|
+
before.dev !== current.dev || before.ino !== current.ino || before.size !== after.size ||
|
|
479
|
+
before.size !== current.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs ||
|
|
480
|
+
before.mtimeMs !== current.mtimeMs || before.ctimeMs !== current.ctimeMs ||
|
|
481
|
+
wrappedDekFilePath(profile) !== target || _state.kind !== 'uninit') {
|
|
482
|
+
throw new Error('changed envelope');
|
|
483
|
+
}
|
|
484
|
+
for (const old of _retiredDeks)
|
|
485
|
+
old.fill(0);
|
|
486
|
+
_retiredDeks = [];
|
|
487
|
+
_state = { kind: 'active', dek: candidate, kekId: kek.id,
|
|
488
|
+
kekKind: typeof kek.kind === 'string' || kek.kind === null ? (kek.kind ?? null) : null, profile };
|
|
489
|
+
candidate = undefined;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
if (Buffer.isBuffer(candidate))
|
|
493
|
+
candidate.fill(0);
|
|
494
|
+
throw new Error('Existing credential envelope is unavailable');
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
if (fd !== undefined)
|
|
498
|
+
fs.closeSync(fd);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/** First enrollment for a reserved desktop background profile. Existing or
|
|
502
|
+
* ambiguous state is opened read-only or refused; it is never re-enrolled.
|
|
503
|
+
* The caller must validate the reservation before calling this function. */
|
|
504
|
+
export async function initNewIndependentCredEnvelope(profile) {
|
|
505
|
+
if (!/^desktop-bg-[a-f0-9]{32}$/.test(profile))
|
|
506
|
+
throw new Error('Background credential enrollment is unavailable');
|
|
507
|
+
if (_state.kind === 'active' && _state.profile === profile)
|
|
508
|
+
return;
|
|
509
|
+
if (_state.kind !== 'uninit')
|
|
510
|
+
throw new Error('Background credential enrollment is unavailable');
|
|
511
|
+
const resolved = resolveProfileStateFile(profile, ACCOUNT_CRED_DEK_WRAPPED);
|
|
512
|
+
const paths = [resolved.canonicalPath, ...(resolved.legacyPath ? [resolved.legacyPath] : [])];
|
|
513
|
+
const present = (file) => {
|
|
514
|
+
try {
|
|
515
|
+
fs.lstatSync(file);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
if (err.code === 'ENOENT')
|
|
520
|
+
return false;
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
if (paths.some(present))
|
|
525
|
+
return initExistingCredEnvelope(profile);
|
|
526
|
+
// A missing DEK alongside a previously written identity or credential is
|
|
527
|
+
// recovery, not first enrollment, even if a retried caller says otherwise.
|
|
528
|
+
const dir = resolved.canonicalDir;
|
|
529
|
+
const stat = fs.lstatSync(dir);
|
|
530
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 ||
|
|
531
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid())) {
|
|
532
|
+
throw new Error('Background credential enrollment is unavailable');
|
|
533
|
+
}
|
|
534
|
+
const artifacts = ['device-private-key', 'device-public-key.jwk', 'auth-binding.json', 'auth.json'];
|
|
535
|
+
const artifactDirs = [dir, ...(resolved.legacyDir ? [resolved.legacyDir] : [])];
|
|
536
|
+
const assertEmpty = () => {
|
|
537
|
+
if (paths.some(present) || artifactDirs.some((root) => artifacts.some((name) => present(path.join(root, name)))) ||
|
|
538
|
+
wrappedDekFilePath(profile) !== resolved.canonicalPath) {
|
|
539
|
+
throw new Error('Background credential enrollment needs recovery');
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
assertEmpty();
|
|
543
|
+
const kek = getKekStrategy();
|
|
544
|
+
if (!kek.unwrapExisting || !(await kek.available()))
|
|
545
|
+
throw new Error('Background credential enrollment is unavailable');
|
|
546
|
+
assertEmpty();
|
|
547
|
+
const dek = generateDek();
|
|
548
|
+
let temp;
|
|
549
|
+
try {
|
|
550
|
+
const wrapped = await kek.wrap(dek, buildCredentialAad(profile));
|
|
551
|
+
// Another enrollment may have won while the platform store was busy.
|
|
552
|
+
// Its file is opened read-only; this process never overwrites it.
|
|
553
|
+
if (paths.some(present))
|
|
554
|
+
return await initExistingCredEnvelope(profile);
|
|
555
|
+
assertEmpty();
|
|
556
|
+
const temporaryPath = path.join(dir, `.cred-dek-enrollment.${randomBytes(12).toString('hex')}.tmp`);
|
|
557
|
+
const fd = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL |
|
|
558
|
+
(fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
559
|
+
temp = temporaryPath;
|
|
560
|
+
try {
|
|
561
|
+
fs.writeFileSync(fd, wrapped);
|
|
562
|
+
fs.fsyncSync(fd);
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
fs.closeSync(fd);
|
|
566
|
+
}
|
|
567
|
+
assertEmpty();
|
|
568
|
+
try {
|
|
569
|
+
fs.linkSync(temp, resolved.canonicalPath);
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
if (err.code !== 'EEXIST')
|
|
573
|
+
throw err;
|
|
574
|
+
}
|
|
575
|
+
const dirFd = fs.openSync(dir, 'r');
|
|
576
|
+
try {
|
|
577
|
+
fs.fsyncSync(dirFd);
|
|
578
|
+
}
|
|
579
|
+
finally {
|
|
580
|
+
fs.closeSync(dirFd);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
dek.fill(0);
|
|
585
|
+
if (temp) {
|
|
586
|
+
fs.unlinkSync(temp);
|
|
587
|
+
const dirFd = fs.openSync(dir, 'r');
|
|
588
|
+
try {
|
|
589
|
+
fs.fsyncSync(dirFd);
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
fs.closeSync(dirFd);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
await initExistingCredEnvelope(profile);
|
|
597
|
+
}
|
|
432
598
|
/**
|
|
433
599
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
434
600
|
* non-uninit returns immediately.
|
|
@@ -440,6 +606,11 @@ function retiredDekFiles(profile) {
|
|
|
440
606
|
* 4. Any throw → disabled+init_failed; transforms stay passthrough.
|
|
441
607
|
*/
|
|
442
608
|
export async function initCredEnvelope(profile) {
|
|
609
|
+
// A reserved background profile has an independent owner and device key.
|
|
610
|
+
// Generic status, startup and CLI paths may read it, but cannot replace a
|
|
611
|
+
// missing or unreadable key. Only validated first enrollment can create one.
|
|
612
|
+
if (/^desktop-bg-[a-f0-9]{32}$/i.test(profile))
|
|
613
|
+
return initExistingCredEnvelope(profile);
|
|
443
614
|
if (_state.kind !== 'uninit')
|
|
444
615
|
return;
|
|
445
616
|
if (typeof profile !== 'string' || profile.length === 0) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DaemonConsentPayload {
|
|
2
|
+
consentVersion: number;
|
|
3
|
+
consentSource: string;
|
|
4
|
+
isLegacy: boolean;
|
|
5
|
+
isReConsent: boolean;
|
|
6
|
+
scopes: string[];
|
|
7
|
+
scopeDescriptions: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
/** A single canonical, bounded representation on both sides of the log. */
|
|
10
|
+
export declare function normalizeDaemonConsent(input: unknown): DaemonConsentPayload | null;
|
|
11
|
+
export interface DaemonConsentIdentity {
|
|
12
|
+
pid: number;
|
|
13
|
+
daemonId: string;
|
|
14
|
+
userId: string;
|
|
15
|
+
origin: string;
|
|
16
|
+
}
|
|
17
|
+
/** The bootstrap secret is never logged. This MAC binds the exact consent,
|
|
18
|
+
* live process, daemon and cloud owner; an old log entry is not authority. */
|
|
19
|
+
export declare function daemonConsentProof(secret: string, identity: DaemonConsentIdentity, payload: DaemonConsentPayload): string;
|
|
20
|
+
export declare function verifyDaemonConsentProof(secret: string, identity: DaemonConsentIdentity, payload: DaemonConsentPayload, proof: string): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/** A single canonical, bounded representation on both sides of the log. */
|
|
3
|
+
export function normalizeDaemonConsent(input) {
|
|
4
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
5
|
+
return null;
|
|
6
|
+
const value = input;
|
|
7
|
+
if (!Number.isSafeInteger(value.consentVersion) || value.consentVersion < 0 ||
|
|
8
|
+
typeof value.consentSource !== 'string' || value.consentSource.length > 64 ||
|
|
9
|
+
typeof value.isLegacy !== 'boolean' || typeof value.isReConsent !== 'boolean' ||
|
|
10
|
+
!Array.isArray(value.scopes) || value.scopes.length > 128 ||
|
|
11
|
+
!value.scopes.every((scope) => typeof scope === 'string' && scope.length <= 256) ||
|
|
12
|
+
!value.scopeDescriptions || typeof value.scopeDescriptions !== 'object' || Array.isArray(value.scopeDescriptions))
|
|
13
|
+
return null;
|
|
14
|
+
const entries = Object.entries(value.scopeDescriptions)
|
|
15
|
+
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
16
|
+
if (entries.length > 128 || entries.some(([key, text]) => key.length > 256 || typeof text !== 'string' || text.length > 1024))
|
|
17
|
+
return null;
|
|
18
|
+
return { consentVersion: value.consentVersion, consentSource: value.consentSource,
|
|
19
|
+
isLegacy: value.isLegacy, isReConsent: value.isReConsent, scopes: value.scopes,
|
|
20
|
+
scopeDescriptions: Object.fromEntries(entries) };
|
|
21
|
+
}
|
|
22
|
+
/** The bootstrap secret is never logged. This MAC binds the exact consent,
|
|
23
|
+
* live process, daemon and cloud owner; an old log entry is not authority. */
|
|
24
|
+
export function daemonConsentProof(secret, identity, payload) {
|
|
25
|
+
const key = Buffer.from(secret, 'base64');
|
|
26
|
+
if (key.length !== 32 || !Number.isSafeInteger(identity.pid) || identity.pid <= 0 ||
|
|
27
|
+
!identity.daemonId || !identity.userId)
|
|
28
|
+
throw new Error('Invalid daemon consent identity');
|
|
29
|
+
const origin = new URL(identity.origin).origin;
|
|
30
|
+
return createHmac('sha256', key).update('skrr-daemon-consent:v1\0').update(JSON.stringify([
|
|
31
|
+
identity.pid, identity.daemonId, identity.userId.toLowerCase(), origin, payload,
|
|
32
|
+
])).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
export function verifyDaemonConsentProof(secret, identity, payload, proof) {
|
|
35
|
+
if (!/^[a-f0-9]{64}$/.test(proof))
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
return timingSafeEqual(Buffer.from(proof, 'hex'), Buffer.from(daemonConsentProof(secret, identity, payload), 'hex'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|