@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
|
@@ -37,6 +37,8 @@ __export(credentialEnvelopeBridge_exports, {
|
|
|
37
37
|
describeCredEnvelopeState: () => describeCredEnvelopeState,
|
|
38
38
|
hasCredEnvelopePathConflict: () => hasCredEnvelopePathConflict,
|
|
39
39
|
initCredEnvelope: () => initCredEnvelope,
|
|
40
|
+
initExistingCredEnvelope: () => initExistingCredEnvelope,
|
|
41
|
+
initNewIndependentCredEnvelope: () => initNewIndependentCredEnvelope,
|
|
40
42
|
isCredEnvelopeActive: () => isCredEnvelopeActive,
|
|
41
43
|
maybeDecryptOnRead: () => maybeDecryptOnRead,
|
|
42
44
|
maybeEncryptForWrite: () => maybeEncryptForWrite,
|
|
@@ -256,7 +258,150 @@ function retiredDekFiles(profile) {
|
|
|
256
258
|
return [];
|
|
257
259
|
}
|
|
258
260
|
}
|
|
261
|
+
async function initExistingCredEnvelope(profile) {
|
|
262
|
+
if (_state.kind === "active" && _state.profile === profile)
|
|
263
|
+
return;
|
|
264
|
+
if (_state.kind !== "uninit" || typeof profile !== "string" || !profile) {
|
|
265
|
+
throw new Error("Existing credential envelope is unavailable");
|
|
266
|
+
}
|
|
267
|
+
let fd;
|
|
268
|
+
let candidate;
|
|
269
|
+
try {
|
|
270
|
+
const target = wrappedDekFilePath(profile);
|
|
271
|
+
fd = import_node_fs.default.openSync(target, import_node_fs.default.constants.O_RDONLY | (import_node_fs.default.constants.O_NOFOLLOW ?? 0));
|
|
272
|
+
const before = import_node_fs.default.fstatSync(fd);
|
|
273
|
+
const safe = (stat) => stat.isFile() && stat.nlink === 1 && stat.size > 0 && stat.size <= 65536 && (stat.mode & 63) === 0 && (typeof process.getuid !== "function" || stat.uid === process.getuid());
|
|
274
|
+
if (!safe(before))
|
|
275
|
+
throw new Error("unsafe envelope");
|
|
276
|
+
const readBounded = () => {
|
|
277
|
+
const bytes = Buffer.alloc(before.size);
|
|
278
|
+
let offset = 0;
|
|
279
|
+
while (offset < bytes.length) {
|
|
280
|
+
const count = import_node_fs.default.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
281
|
+
if (count <= 0)
|
|
282
|
+
throw new Error("changed envelope");
|
|
283
|
+
offset += count;
|
|
284
|
+
}
|
|
285
|
+
return bytes;
|
|
286
|
+
};
|
|
287
|
+
const wrapped = readBounded();
|
|
288
|
+
const kek = (0, import_kek.getKekStrategy)();
|
|
289
|
+
if (!kek.unwrapExisting || !await kek.available())
|
|
290
|
+
throw new Error("read-only KEK unavailable");
|
|
291
|
+
candidate = await kek.unwrapExisting(wrapped, (0, import_credentialEnvelope.buildCredentialAad)(profile));
|
|
292
|
+
if (!Buffer.isBuffer(candidate) || candidate.length !== 32)
|
|
293
|
+
throw new Error("invalid envelope");
|
|
294
|
+
if (!readBounded().equals(wrapped))
|
|
295
|
+
throw new Error("changed envelope");
|
|
296
|
+
const after = import_node_fs.default.fstatSync(fd);
|
|
297
|
+
const current = import_node_fs.default.lstatSync(target);
|
|
298
|
+
if (!safe(after) || !safe(current) || before.dev !== after.dev || before.ino !== after.ino || before.dev !== current.dev || before.ino !== current.ino || before.size !== after.size || before.size !== current.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.mtimeMs !== current.mtimeMs || before.ctimeMs !== current.ctimeMs || wrappedDekFilePath(profile) !== target || _state.kind !== "uninit") {
|
|
299
|
+
throw new Error("changed envelope");
|
|
300
|
+
}
|
|
301
|
+
for (const old of _retiredDeks)
|
|
302
|
+
old.fill(0);
|
|
303
|
+
_retiredDeks = [];
|
|
304
|
+
_state = {
|
|
305
|
+
kind: "active",
|
|
306
|
+
dek: candidate,
|
|
307
|
+
kekId: kek.id,
|
|
308
|
+
kekKind: typeof kek.kind === "string" || kek.kind === null ? kek.kind ?? null : null,
|
|
309
|
+
profile
|
|
310
|
+
};
|
|
311
|
+
candidate = void 0;
|
|
312
|
+
} catch {
|
|
313
|
+
if (Buffer.isBuffer(candidate))
|
|
314
|
+
candidate.fill(0);
|
|
315
|
+
throw new Error("Existing credential envelope is unavailable");
|
|
316
|
+
} finally {
|
|
317
|
+
if (fd !== void 0)
|
|
318
|
+
import_node_fs.default.closeSync(fd);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function initNewIndependentCredEnvelope(profile) {
|
|
322
|
+
if (!/^desktop-bg-[a-f0-9]{32}$/.test(profile))
|
|
323
|
+
throw new Error("Background credential enrollment is unavailable");
|
|
324
|
+
if (_state.kind === "active" && _state.profile === profile)
|
|
325
|
+
return;
|
|
326
|
+
if (_state.kind !== "uninit")
|
|
327
|
+
throw new Error("Background credential enrollment is unavailable");
|
|
328
|
+
const resolved = (0, import_profileStateDir.resolveProfileStateFile)(profile, ACCOUNT_CRED_DEK_WRAPPED);
|
|
329
|
+
const paths = [resolved.canonicalPath, ...resolved.legacyPath ? [resolved.legacyPath] : []];
|
|
330
|
+
const present = (file) => {
|
|
331
|
+
try {
|
|
332
|
+
import_node_fs.default.lstatSync(file);
|
|
333
|
+
return true;
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (err.code === "ENOENT")
|
|
336
|
+
return false;
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
if (paths.some(present))
|
|
341
|
+
return initExistingCredEnvelope(profile);
|
|
342
|
+
const dir = resolved.canonicalDir;
|
|
343
|
+
const stat = import_node_fs.default.lstatSync(dir);
|
|
344
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
345
|
+
throw new Error("Background credential enrollment is unavailable");
|
|
346
|
+
}
|
|
347
|
+
const artifacts = ["device-private-key", "device-public-key.jwk", "auth-binding.json", "auth.json"];
|
|
348
|
+
const artifactDirs = [dir, ...resolved.legacyDir ? [resolved.legacyDir] : []];
|
|
349
|
+
const assertEmpty = () => {
|
|
350
|
+
if (paths.some(present) || artifactDirs.some((root) => artifacts.some((name) => present(import_node_path.default.join(root, name)))) || wrappedDekFilePath(profile) !== resolved.canonicalPath) {
|
|
351
|
+
throw new Error("Background credential enrollment needs recovery");
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
assertEmpty();
|
|
355
|
+
const kek = (0, import_kek.getKekStrategy)();
|
|
356
|
+
if (!kek.unwrapExisting || !await kek.available())
|
|
357
|
+
throw new Error("Background credential enrollment is unavailable");
|
|
358
|
+
assertEmpty();
|
|
359
|
+
const dek = (0, import_credentialEnvelope.generateDek)();
|
|
360
|
+
let temp;
|
|
361
|
+
try {
|
|
362
|
+
const wrapped = await kek.wrap(dek, (0, import_credentialEnvelope.buildCredentialAad)(profile));
|
|
363
|
+
if (paths.some(present))
|
|
364
|
+
return await initExistingCredEnvelope(profile);
|
|
365
|
+
assertEmpty();
|
|
366
|
+
const temporaryPath = import_node_path.default.join(dir, `.cred-dek-enrollment.${(0, import_node_crypto.randomBytes)(12).toString("hex")}.tmp`);
|
|
367
|
+
const fd = import_node_fs.default.openSync(temporaryPath, 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);
|
|
368
|
+
temp = temporaryPath;
|
|
369
|
+
try {
|
|
370
|
+
import_node_fs.default.writeFileSync(fd, wrapped);
|
|
371
|
+
import_node_fs.default.fsyncSync(fd);
|
|
372
|
+
} finally {
|
|
373
|
+
import_node_fs.default.closeSync(fd);
|
|
374
|
+
}
|
|
375
|
+
assertEmpty();
|
|
376
|
+
try {
|
|
377
|
+
import_node_fs.default.linkSync(temp, resolved.canonicalPath);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
if (err.code !== "EEXIST")
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
const dirFd = import_node_fs.default.openSync(dir, "r");
|
|
383
|
+
try {
|
|
384
|
+
import_node_fs.default.fsyncSync(dirFd);
|
|
385
|
+
} finally {
|
|
386
|
+
import_node_fs.default.closeSync(dirFd);
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
dek.fill(0);
|
|
390
|
+
if (temp) {
|
|
391
|
+
import_node_fs.default.unlinkSync(temp);
|
|
392
|
+
const dirFd = import_node_fs.default.openSync(dir, "r");
|
|
393
|
+
try {
|
|
394
|
+
import_node_fs.default.fsyncSync(dirFd);
|
|
395
|
+
} finally {
|
|
396
|
+
import_node_fs.default.closeSync(dirFd);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
await initExistingCredEnvelope(profile);
|
|
401
|
+
}
|
|
259
402
|
async function initCredEnvelope(profile) {
|
|
403
|
+
if (/^desktop-bg-[a-f0-9]{32}$/i.test(profile))
|
|
404
|
+
return initExistingCredEnvelope(profile);
|
|
260
405
|
if (_state.kind !== "uninit")
|
|
261
406
|
return;
|
|
262
407
|
if (typeof profile !== "string" || profile.length === 0) {
|
|
@@ -629,6 +774,8 @@ function __wrappedDekFilePathForTest(profile) {
|
|
|
629
774
|
describeCredEnvelopeState,
|
|
630
775
|
hasCredEnvelopePathConflict,
|
|
631
776
|
initCredEnvelope,
|
|
777
|
+
initExistingCredEnvelope,
|
|
778
|
+
initNewIndependentCredEnvelope,
|
|
632
779
|
isCredEnvelopeActive,
|
|
633
780
|
maybeDecryptOnRead,
|
|
634
781
|
maybeEncryptForWrite,
|
|
@@ -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,72 @@
|
|
|
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 daemonConsentProof_exports = {};
|
|
20
|
+
__export(daemonConsentProof_exports, {
|
|
21
|
+
daemonConsentProof: () => daemonConsentProof,
|
|
22
|
+
normalizeDaemonConsent: () => normalizeDaemonConsent,
|
|
23
|
+
verifyDaemonConsentProof: () => verifyDaemonConsentProof
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(daemonConsentProof_exports);
|
|
26
|
+
var import_node_crypto = require("node:crypto");
|
|
27
|
+
function normalizeDaemonConsent(input) {
|
|
28
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
29
|
+
return null;
|
|
30
|
+
const value = input;
|
|
31
|
+
if (!Number.isSafeInteger(value.consentVersion) || value.consentVersion < 0 || typeof value.consentSource !== "string" || value.consentSource.length > 64 || typeof value.isLegacy !== "boolean" || typeof value.isReConsent !== "boolean" || !Array.isArray(value.scopes) || value.scopes.length > 128 || !value.scopes.every((scope) => typeof scope === "string" && scope.length <= 256) || !value.scopeDescriptions || typeof value.scopeDescriptions !== "object" || Array.isArray(value.scopeDescriptions))
|
|
32
|
+
return null;
|
|
33
|
+
const entries = Object.entries(value.scopeDescriptions).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
34
|
+
if (entries.length > 128 || entries.some(([key, text]) => key.length > 256 || typeof text !== "string" || text.length > 1024))
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
consentVersion: value.consentVersion,
|
|
38
|
+
consentSource: value.consentSource,
|
|
39
|
+
isLegacy: value.isLegacy,
|
|
40
|
+
isReConsent: value.isReConsent,
|
|
41
|
+
scopes: value.scopes,
|
|
42
|
+
scopeDescriptions: Object.fromEntries(entries)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function daemonConsentProof(secret, identity, payload) {
|
|
46
|
+
const key = Buffer.from(secret, "base64");
|
|
47
|
+
if (key.length !== 32 || !Number.isSafeInteger(identity.pid) || identity.pid <= 0 || !identity.daemonId || !identity.userId)
|
|
48
|
+
throw new Error("Invalid daemon consent identity");
|
|
49
|
+
const origin = new URL(identity.origin).origin;
|
|
50
|
+
return (0, import_node_crypto.createHmac)("sha256", key).update("skrr-daemon-consent:v1\0").update(JSON.stringify([
|
|
51
|
+
identity.pid,
|
|
52
|
+
identity.daemonId,
|
|
53
|
+
identity.userId.toLowerCase(),
|
|
54
|
+
origin,
|
|
55
|
+
payload
|
|
56
|
+
])).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
function verifyDaemonConsentProof(secret, identity, payload, proof) {
|
|
59
|
+
if (!/^[a-f0-9]{64}$/.test(proof))
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
return (0, import_node_crypto.timingSafeEqual)(Buffer.from(proof, "hex"), Buffer.from(daemonConsentProof(secret, identity, payload), "hex"));
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
daemonConsentProof,
|
|
70
|
+
normalizeDaemonConsent,
|
|
71
|
+
verifyDaemonConsentProof
|
|
72
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const DESKTOP_DAEMON_PROOF_NAMESPACE = "/desktop-proof-v1";
|
|
2
|
+
export declare const DESKTOP_DAEMON_RENEWAL_NAMESPACE = "/desktop-renewal-v1";
|
|
3
|
+
export interface DesktopDaemonOwner {
|
|
4
|
+
userId: string;
|
|
5
|
+
serverUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DesktopDaemonChallenge {
|
|
8
|
+
owner: DesktopDaemonOwner;
|
|
9
|
+
port: number;
|
|
10
|
+
clientNonce: string;
|
|
11
|
+
serverNonce: string;
|
|
12
|
+
}
|
|
13
|
+
/** The same canonical owner is used by the daemon's owner boundary and MAC. */
|
|
14
|
+
export declare function normalizeDesktopDaemonOwner(input: unknown): DesktopDaemonOwner | null;
|
|
15
|
+
/** Role separation prevents reflecting the daemon's proof as client proof. */
|
|
16
|
+
export declare function desktopDaemonProof(secret: string, role: 'daemon' | 'desktop', challenge: DesktopDaemonChallenge): string;
|
|
17
|
+
/** Renewal uses a distinct domain so a proof for this narrow channel cannot
|
|
18
|
+
* be reflected or replayed as a normal desktop attachment. */
|
|
19
|
+
export declare function desktopRenewalProof(secret: string, role: 'daemon' | 'desktop', challenge: DesktopDaemonChallenge): string;
|
|
20
|
+
export declare function verifyDesktopDaemonProof(secret: string, role: 'daemon' | 'desktop', challenge: DesktopDaemonChallenge, proof: unknown): boolean;
|
|
21
|
+
export declare function verifyDesktopRenewalProof(secret: string, role: 'daemon' | 'desktop', challenge: DesktopDaemonChallenge, proof: unknown): boolean;
|
|
@@ -0,0 +1,96 @@
|
|
|
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 desktopDaemonProof_exports = {};
|
|
20
|
+
__export(desktopDaemonProof_exports, {
|
|
21
|
+
DESKTOP_DAEMON_PROOF_NAMESPACE: () => DESKTOP_DAEMON_PROOF_NAMESPACE,
|
|
22
|
+
DESKTOP_DAEMON_RENEWAL_NAMESPACE: () => DESKTOP_DAEMON_RENEWAL_NAMESPACE,
|
|
23
|
+
desktopDaemonProof: () => desktopDaemonProof,
|
|
24
|
+
desktopRenewalProof: () => desktopRenewalProof,
|
|
25
|
+
normalizeDesktopDaemonOwner: () => normalizeDesktopDaemonOwner,
|
|
26
|
+
verifyDesktopDaemonProof: () => verifyDesktopDaemonProof,
|
|
27
|
+
verifyDesktopRenewalProof: () => verifyDesktopRenewalProof
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(desktopDaemonProof_exports);
|
|
30
|
+
var import_node_crypto = require("node:crypto");
|
|
31
|
+
const DESKTOP_DAEMON_PROOF_NAMESPACE = "/desktop-proof-v1";
|
|
32
|
+
const DESKTOP_DAEMON_RENEWAL_NAMESPACE = "/desktop-renewal-v1";
|
|
33
|
+
function normalizeDesktopDaemonOwner(input) {
|
|
34
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
35
|
+
return null;
|
|
36
|
+
const value = input;
|
|
37
|
+
if (typeof value.userId !== "string" || !value.userId || value.userId.length > 128 || typeof value.serverUrl !== "string" || value.serverUrl.length > 2048)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(value.serverUrl);
|
|
41
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
42
|
+
return null;
|
|
43
|
+
return { userId: value.userId, serverUrl: url.origin };
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function desktopDaemonProof(secret, role, challenge) {
|
|
49
|
+
return proofForPurpose(secret, role, challenge, "skrr-desktop-daemon-proof:v1\0");
|
|
50
|
+
}
|
|
51
|
+
function desktopRenewalProof(secret, role, challenge) {
|
|
52
|
+
return proofForPurpose(secret, role, challenge, "skrr-desktop-renewal-proof:v1\0");
|
|
53
|
+
}
|
|
54
|
+
function proofForPurpose(secret, role, challenge, domain) {
|
|
55
|
+
const key = Buffer.from(secret, "base64");
|
|
56
|
+
const owner = normalizeDesktopDaemonOwner(challenge.owner);
|
|
57
|
+
if (key.length !== 32 || key.toString("base64") !== secret || !owner || !Number.isInteger(challenge.port) || challenge.port < 1 || challenge.port > 65535 || !/^[a-f0-9]{64}$/.test(challenge.clientNonce) || !/^[a-f0-9]{64}$/.test(challenge.serverNonce)) {
|
|
58
|
+
throw new Error("Invalid desktop daemon challenge");
|
|
59
|
+
}
|
|
60
|
+
return (0, import_node_crypto.createHmac)("sha256", key).update(domain).update(JSON.stringify([
|
|
61
|
+
role,
|
|
62
|
+
challenge.port,
|
|
63
|
+
owner.userId,
|
|
64
|
+
owner.serverUrl,
|
|
65
|
+
challenge.clientNonce,
|
|
66
|
+
challenge.serverNonce
|
|
67
|
+
])).digest("hex");
|
|
68
|
+
}
|
|
69
|
+
function verifyDesktopDaemonProof(secret, role, challenge, proof) {
|
|
70
|
+
if (typeof proof !== "string" || !/^[a-f0-9]{64}$/.test(proof))
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
return (0, import_node_crypto.timingSafeEqual)(Buffer.from(proof, "hex"), Buffer.from(desktopDaemonProof(secret, role, challenge), "hex"));
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function verifyDesktopRenewalProof(secret, role, challenge, proof) {
|
|
79
|
+
if (typeof proof !== "string" || !/^[a-f0-9]{64}$/.test(proof))
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
return (0, import_node_crypto.timingSafeEqual)(Buffer.from(proof, "hex"), Buffer.from(desktopRenewalProof(secret, role, challenge), "hex"));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
88
|
+
0 && (module.exports = {
|
|
89
|
+
DESKTOP_DAEMON_PROOF_NAMESPACE,
|
|
90
|
+
DESKTOP_DAEMON_RENEWAL_NAMESPACE,
|
|
91
|
+
desktopDaemonProof,
|
|
92
|
+
desktopRenewalProof,
|
|
93
|
+
normalizeDesktopDaemonOwner,
|
|
94
|
+
verifyDesktopDaemonProof,
|
|
95
|
+
verifyDesktopRenewalProof
|
|
96
|
+
});
|
|
@@ -51,9 +51,8 @@ export declare function describeDeviceKeySplit(profile: string): {
|
|
|
51
51
|
* `createIfMissing: false` stops before step 2 (disabled+no_stored_key) and
|
|
52
52
|
* never replaces anything: for a short-lived process that must only READ the
|
|
53
53
|
* daemon's key while the daemon itself may be creating it (OSK-15990's
|
|
54
|
-
* `skrrd device-proof`).
|
|
55
|
-
*
|
|
56
|
-
* memory and the disk another.
|
|
54
|
+
* `skrrd device-proof`). Readers and writers both take the profile lock so
|
|
55
|
+
* they cannot observe an incomplete write from another updated process.
|
|
57
56
|
*/
|
|
58
57
|
export declare function initDeviceIdentity(profile: string, options?: {
|
|
59
58
|
recoverUnreadable?: boolean;
|
|
@@ -47,6 +47,7 @@ __export(deviceIdentityBridge_exports, {
|
|
|
47
47
|
module.exports = __toCommonJS(deviceIdentityBridge_exports);
|
|
48
48
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
49
49
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
50
|
+
var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
|
|
50
51
|
var import_deviceKey = require("./deviceKey.js");
|
|
51
52
|
var import_credentialEnvelopeBridge = require("./credentialEnvelopeBridge.js");
|
|
52
53
|
var import_profileStateDir = require("./profileStateDir.js");
|
|
@@ -63,6 +64,22 @@ const DEVICE_IDENTITY_FILES = [
|
|
|
63
64
|
];
|
|
64
65
|
const DAEMON_NOT_FOUND_BACKOFF_MS = 15 * 60 * 1e3;
|
|
65
66
|
let _state = { kind: "uninit" };
|
|
67
|
+
let _initializing = null;
|
|
68
|
+
async function withDeviceIdentityLock(profile, body) {
|
|
69
|
+
const dir = (0, import_profileStateDir.profileStateDir)(profile);
|
|
70
|
+
import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
71
|
+
const release = await import_proper_lockfile.default.lock(dir, {
|
|
72
|
+
realpath: false,
|
|
73
|
+
lockfilePath: import_node_path.default.join(dir, ".device-identity.lock"),
|
|
74
|
+
stale: 10 * 60 * 1e3,
|
|
75
|
+
retries: { retries: 20, minTimeout: 100, maxTimeout: 500, factor: 1.25 }
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
return await body();
|
|
79
|
+
} finally {
|
|
80
|
+
await release();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
66
83
|
function __setStateForTest(next) {
|
|
67
84
|
const prev = _state;
|
|
68
85
|
_state = next;
|
|
@@ -72,6 +89,8 @@ function __getStateForTest() {
|
|
|
72
89
|
return _state;
|
|
73
90
|
}
|
|
74
91
|
function profileConfigDir(profile) {
|
|
92
|
+
if (/^desktop-bg-[a-f0-9]{32}$/i.test(profile))
|
|
93
|
+
return (0, import_profileStateDir.profileStateDir)(profile);
|
|
75
94
|
return (0, import_profileStateDir.resolveProfileStateDir)(profile, PRIVATE_KEY_FILENAME).dir;
|
|
76
95
|
}
|
|
77
96
|
function publicKeyPath(profile) {
|
|
@@ -162,14 +181,49 @@ function persistKeypair(profile, kp) {
|
|
|
162
181
|
});
|
|
163
182
|
return { envelopeProtected };
|
|
164
183
|
}
|
|
184
|
+
function readIndependentIdentityFile(file, sealed) {
|
|
185
|
+
if (typeof import_node_fs.default.constants.O_NOFOLLOW !== "number" || typeof import_node_fs.default.constants.O_NONBLOCK !== "number")
|
|
186
|
+
return null;
|
|
187
|
+
let fd;
|
|
188
|
+
try {
|
|
189
|
+
fd = import_node_fs.default.openSync(file, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW | import_node_fs.default.constants.O_NONBLOCK);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const before = import_node_fs.default.fstatSync(fd);
|
|
195
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > 16384 || (before.mode & 63) !== 0 || typeof process.getuid === "function" && before.uid !== process.getuid())
|
|
196
|
+
return null;
|
|
197
|
+
const bytes = Buffer.alloc(before.size);
|
|
198
|
+
let offset = 0;
|
|
199
|
+
while (offset < bytes.length) {
|
|
200
|
+
const count = import_node_fs.default.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
201
|
+
if (count <= 0)
|
|
202
|
+
return null;
|
|
203
|
+
offset += count;
|
|
204
|
+
}
|
|
205
|
+
const after = import_node_fs.default.fstatSync(fd);
|
|
206
|
+
const current = import_node_fs.default.lstatSync(file);
|
|
207
|
+
if (!current.isFile() || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size || current.nlink !== before.nlink || current.mode !== before.mode || current.uid !== before.uid || current.mtimeMs !== before.mtimeMs || current.ctimeMs !== before.ctimeMs || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.nlink !== after.nlink || before.mode !== after.mode || before.uid !== after.uid || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
208
|
+
return null;
|
|
209
|
+
const value = bytes.toString("utf8");
|
|
210
|
+
return sealed && !value.startsWith("osk-env1:") ? null : value;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
} finally {
|
|
214
|
+
import_node_fs.default.closeSync(fd);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
165
217
|
function loadKeypair(profile) {
|
|
166
218
|
const pubPath = publicKeyPath(profile);
|
|
167
219
|
const privPath = privateKeyPath(profile);
|
|
168
|
-
|
|
220
|
+
const independent = /^desktop-bg-[a-f0-9]{32}$/i.test(profile);
|
|
221
|
+
if (!independent && (!import_node_fs.default.existsSync(pubPath) || !import_node_fs.default.existsSync(privPath)))
|
|
222
|
+
return null;
|
|
223
|
+
const pubRaw = independent ? readIndependentIdentityFile(pubPath, false) : import_node_fs.default.readFileSync(pubPath, "utf-8");
|
|
224
|
+
const privRaw = independent ? readIndependentIdentityFile(privPath, true) : import_node_fs.default.readFileSync(privPath, "utf-8");
|
|
225
|
+
if (pubRaw === null || privRaw === null)
|
|
169
226
|
return null;
|
|
170
|
-
}
|
|
171
|
-
const pubRaw = import_node_fs.default.readFileSync(pubPath, "utf-8");
|
|
172
|
-
const privRaw = import_node_fs.default.readFileSync(privPath, "utf-8");
|
|
173
227
|
let publicJwk;
|
|
174
228
|
let storedThumbprint;
|
|
175
229
|
try {
|
|
@@ -270,14 +324,38 @@ async function consolidateDeviceIdentity(profile) {
|
|
|
270
324
|
}
|
|
271
325
|
}
|
|
272
326
|
async function initDeviceIdentity(profile, options = {}) {
|
|
327
|
+
if (_initializing)
|
|
328
|
+
return _initializing;
|
|
273
329
|
if (_state.kind !== "uninit")
|
|
274
330
|
return;
|
|
275
|
-
if (typeof profile !== "string" ||
|
|
331
|
+
if (typeof profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(profile)) {
|
|
276
332
|
_state = { kind: "disabled", reason: "init_failed" };
|
|
277
|
-
emit("init_failed", false, { reason: "
|
|
333
|
+
emit("init_failed", false, { reason: "invalid_profile" });
|
|
278
334
|
return;
|
|
279
335
|
}
|
|
280
|
-
|
|
336
|
+
const job = withDeviceIdentityLock(profile, async () => {
|
|
337
|
+
if (_state.kind === "uninit")
|
|
338
|
+
await initDeviceIdentityLocked(profile, options);
|
|
339
|
+
}).catch((err) => {
|
|
340
|
+
_state = { kind: "disabled", reason: "init_failed" };
|
|
341
|
+
emit("init_failed", false, {
|
|
342
|
+
profile,
|
|
343
|
+
reason: "identity_lock_unavailable",
|
|
344
|
+
message: err instanceof Error ? err.message : "unknown"
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
_initializing = job;
|
|
348
|
+
try {
|
|
349
|
+
await job;
|
|
350
|
+
} finally {
|
|
351
|
+
if (_initializing === job)
|
|
352
|
+
_initializing = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function initDeviceIdentityLocked(profile, options = {}) {
|
|
356
|
+
const independent = /^desktop-bg-[a-f0-9]{32}$/i.test(profile);
|
|
357
|
+
if (!independent)
|
|
358
|
+
await consolidateDeviceIdentity(profile);
|
|
281
359
|
let keypair = null;
|
|
282
360
|
try {
|
|
283
361
|
keypair = loadKeypair(profile);
|
|
@@ -300,12 +378,12 @@ async function initDeviceIdentity(profile, options = {}) {
|
|
|
300
378
|
return;
|
|
301
379
|
}
|
|
302
380
|
const hasStoredKey = storedKeyMaterialExists(profile);
|
|
303
|
-
if (hasStoredKey && !options.recoverUnreadable) {
|
|
381
|
+
if (hasStoredKey && (!options.recoverUnreadable || independent)) {
|
|
304
382
|
_state = { kind: "disabled", reason: "unreadable_key" };
|
|
305
383
|
emit("recovery_required", false, { profile, reason: "stored_key_unreadable" });
|
|
306
384
|
return;
|
|
307
385
|
}
|
|
308
|
-
if (options.createIfMissing === false) {
|
|
386
|
+
if (options.createIfMissing === false || independent && options.createIfMissing !== true) {
|
|
309
387
|
_state = { kind: "disabled", reason: "no_stored_key" };
|
|
310
388
|
emit("no_stored_key", false, { profile });
|
|
311
389
|
return;
|
|
@@ -347,21 +425,25 @@ async function initDeviceIdentity(profile, options = {}) {
|
|
|
347
425
|
});
|
|
348
426
|
}
|
|
349
427
|
async function resetDeviceIdentity(opts) {
|
|
428
|
+
if (_initializing)
|
|
429
|
+
await _initializing;
|
|
350
430
|
if (opts?.clearOnDisk && opts.profile) {
|
|
351
431
|
const targets = [PUBLIC_KEY_FILENAME, PRIVATE_KEY_FILENAME].flatMap((name) => (0, import_profileStateDir.profileStateFilePathsToClear)(opts.profile, name));
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
432
|
+
await withDeviceIdentityLock(opts.profile, async () => {
|
|
433
|
+
for (const p of targets) {
|
|
434
|
+
try {
|
|
435
|
+
import_node_fs.default.unlinkSync(p);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
if (err.code !== "ENOENT") {
|
|
438
|
+
emit("clear_failed", false, {
|
|
439
|
+
profile: opts.profile,
|
|
440
|
+
file: import_node_path.default.basename(p),
|
|
441
|
+
message: err?.message ?? "unknown"
|
|
442
|
+
});
|
|
443
|
+
}
|
|
362
444
|
}
|
|
363
445
|
}
|
|
364
|
-
}
|
|
446
|
+
});
|
|
365
447
|
}
|
|
366
448
|
_state = { kind: "uninit" };
|
|
367
449
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The desktop reserves a new profile for every background authorization. */
|
|
2
|
+
export declare const INDEPENDENT_MACHINE_PROFILE_RE: RegExp;
|
|
3
|
+
export declare const INDEPENDENT_MACHINE_RESERVATION_FILE = "independent-machine-attempt.json";
|
|
4
|
+
export interface IndependentMachineReservation {
|
|
5
|
+
version: 2;
|
|
6
|
+
profile: string;
|
|
7
|
+
ownerId: string;
|
|
8
|
+
origin: string;
|
|
9
|
+
secretHash: string;
|
|
10
|
+
encryptedSecret: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
/** Reserve a previously absent profile and persist the encrypted attempt secret.
|
|
14
|
+
* `encrypt` must use the platform secure store and throw if it is unavailable. */
|
|
15
|
+
export declare function reserveIndependentMachineProfile(options: {
|
|
16
|
+
root: string;
|
|
17
|
+
ownerId: string;
|
|
18
|
+
origin: string;
|
|
19
|
+
encrypt: (secret: string) => Buffer;
|
|
20
|
+
}): {
|
|
21
|
+
reservation: IndependentMachineReservation;
|
|
22
|
+
secret: string;
|
|
23
|
+
};
|
|
24
|
+
/** A saved reservation is a selection hint, never proof of machine ownership. */
|
|
25
|
+
export declare function readIndependentMachineReservation(root: string, profile: string): IndependentMachineReservation | null;
|
|
26
|
+
/** Confirm a one-shot signer is acting on the desktop's reserved attempt. */
|
|
27
|
+
export declare function verifyIndependentMachineReservation(root: string, profile: string, secret: unknown): boolean;
|