@skrr-ai/cli 0.1.120 → 0.1.122

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.
Files changed (80) hide show
  1. package/dist/base-command.js +11 -2
  2. package/dist/commands/actions/publish.js +4 -0
  3. package/dist/commands/agent/message/context.d.ts +1 -0
  4. package/dist/commands/agent/message/context.js +3 -2
  5. package/dist/commands/agent/message/send.js +6 -1
  6. package/dist/commands/search/agents.js +5 -1
  7. package/dist/commands/search/all.js +74 -22
  8. package/dist/commands/search/skills.d.ts +1 -0
  9. package/dist/commands/search/skills.js +12 -1
  10. package/dist/commands/search/spaces.d.ts +8 -1
  11. package/dist/commands/search/spaces.js +50 -12
  12. package/dist/commands/search/tasks.d.ts +13 -2
  13. package/dist/commands/search/tasks.js +78 -14
  14. package/dist/commands/search/teams.js +5 -1
  15. package/dist/commands/search/workspace.d.ts +19 -0
  16. package/dist/commands/search/workspace.js +126 -0
  17. package/dist/commands/skills/browse.d.ts +1 -0
  18. package/dist/commands/skills/browse.js +8 -1
  19. package/dist/commands/skills/search.d.ts +1 -0
  20. package/dist/commands/skills/search.js +12 -1
  21. package/dist/commands/spaces/list.d.ts +1 -0
  22. package/dist/commands/spaces/list.js +12 -3
  23. package/dist/commands/spaces/search.d.ts +1 -0
  24. package/dist/commands/spaces/search.js +9 -0
  25. package/dist/commands/store/gallery-admissions/approve.d.ts +16 -0
  26. package/dist/commands/store/gallery-admissions/approve.js +36 -0
  27. package/dist/commands/store/gallery-admissions/list.d.ts +9 -0
  28. package/dist/commands/store/gallery-admissions/list.js +49 -0
  29. package/dist/commands/store/gallery-admissions/preview.d.ts +15 -0
  30. package/dist/commands/store/gallery-admissions/preview.js +48 -0
  31. package/dist/commands/store/gallery-admissions/refuse.d.ts +15 -0
  32. package/dist/commands/store/gallery-admissions/refuse.js +35 -0
  33. package/dist/commands/store/gallery-admissions/show.d.ts +12 -0
  34. package/dist/commands/store/gallery-admissions/show.js +55 -0
  35. package/dist/commands/tasks/list.js +8 -0
  36. package/dist/commands/tasks/search.d.ts +1 -0
  37. package/dist/commands/tasks/search.js +9 -0
  38. package/dist/generated/agent-guides.json +11 -11
  39. package/dist/lib/api-error-reason.d.ts +2 -0
  40. package/dist/lib/api-error-reason.js +14 -0
  41. package/dist/lib/api-fetch.js +3 -0
  42. package/dist/lib/command-miss.js +1 -0
  43. package/dist/lib/search-display.d.ts +7 -0
  44. package/dist/lib/search-display.js +31 -0
  45. package/dist/lib/skill-store-pagination.d.ts +8 -0
  46. package/dist/lib/skill-store-pagination.js +35 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +10 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonConsentProof.d.ts +20 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonConsentProof.js +72 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopDaemonProof.d.ts +21 -0
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopDaemonProof.js +96 -0
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +2 -3
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +102 -20
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.d.ts +27 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +161 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineScopes.d.ts +26 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineScopes.js +41 -0
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +15 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +20 -4
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +10 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +172 -1
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonConsentProof.d.ts +20 -0
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonConsentProof.js +43 -0
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopDaemonProof.d.ts +21 -0
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopDaemonProof.js +62 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +2 -3
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +133 -25
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.d.ts +27 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +132 -0
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineScopes.d.ts +26 -0
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineScopes.js +15 -0
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -1
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -1
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +28 -15
  77. package/dist/node_modules/@skrr-ai/auth-core/package.json +31 -1
  78. package/dist/node_modules/@skrr-ai/data-provider/index.js +7728 -7666
  79. package/oclif.manifest.json +1180 -101
  80. 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`). The persist below is not an exclusive create, so a
55
- * second creator racing the daemon could leave the daemon holding one key in
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
- if (!import_node_fs.default.existsSync(pubPath) || !import_node_fs.default.existsSync(privPath)) {
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" || profile.length === 0) {
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: "empty_profile" });
333
+ emit("init_failed", false, { reason: "invalid_profile" });
278
334
  return;
279
335
  }
280
- await consolidateDeviceIdentity(profile);
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
- for (const p of targets) {
353
- try {
354
- import_node_fs.default.unlinkSync(p);
355
- } catch (err) {
356
- if (err.code !== "ENOENT") {
357
- emit("clear_failed", false, {
358
- profile: opts.profile,
359
- file: import_node_path.default.basename(p),
360
- message: err?.message ?? "unknown"
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;