sentinelayer-cli 0.9.7 → 0.9.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -17,6 +17,8 @@ const SESSION_WARNING_ALLOWED_FIELDS = new Set([
17
17
  "codeHint",
18
18
  "requestIdHash",
19
19
  ]);
20
+ const emittedSessionWarningKeys = new Set();
21
+ let keytarClientOverrideForTests;
20
22
 
21
23
  function nowIso() {
22
24
  return new Date().toISOString();
@@ -69,26 +71,45 @@ function sanitizeSessionWarningDetails(details) {
69
71
 
70
72
  function emitSessionWarning(code, details = {}) {
71
73
  const sanitizedDetails = sanitizeSessionWarningDetails(details);
74
+ const normalizedCode = String(code || "SESSION_WARNING").toUpperCase();
75
+ const allowedDetails = {};
76
+ for (const [key, value] of Object.entries(sanitizedDetails)) {
77
+ allowedDetails[key] = SESSION_WARNING_ALLOWED_FIELDS.has(key) ? value : "[OMITTED]";
78
+ }
79
+ const warningKey = `${normalizedCode}:${JSON.stringify(allowedDetails)}`;
80
+ if (emittedSessionWarningKeys.has(warningKey)) {
81
+ return;
82
+ }
83
+ emittedSessionWarningKeys.add(warningKey);
84
+
72
85
  const payload = {
73
86
  level: "warn",
74
- code: String(code || "SESSION_WARNING").toUpperCase(),
87
+ code: normalizedCode,
75
88
  warningId: createSessionWarningId(),
76
89
  timestamp: nowIso(),
77
90
  };
78
- for (const [key, value] of Object.entries(sanitizedDetails)) {
79
- if (SESSION_WARNING_ALLOWED_FIELDS.has(key)) {
80
- payload[key] = value;
81
- } else {
82
- payload[key] = "[OMITTED]";
83
- }
91
+ for (const [key, value] of Object.entries(allowedDetails)) {
92
+ payload[key] = value;
84
93
  }
85
94
  try {
86
- console.warn(`${SESSION_WARNING_PREFIX} ${JSON.stringify(payload)}`);
95
+ process.stderr.write(`${SESSION_WARNING_PREFIX} ${JSON.stringify(payload)}\n`);
87
96
  } catch {
88
- console.warn(`${SESSION_WARNING_PREFIX} ${payload.code}`);
97
+ console.error(`${SESSION_WARNING_PREFIX} ${payload.code}`);
89
98
  }
90
99
  }
91
100
 
101
+ export function resetSessionWarningsForTests() {
102
+ emittedSessionWarningKeys.clear();
103
+ }
104
+
105
+ export function setKeytarClientForTests(client) {
106
+ const previous = keytarClientOverrideForTests;
107
+ keytarClientOverrideForTests = client || null;
108
+ return () => {
109
+ keytarClientOverrideForTests = previous;
110
+ };
111
+ }
112
+
92
113
  function resolveHomeDir(homeDir) {
93
114
  return path.resolve(String(homeDir || os.homedir()));
94
115
  }
@@ -356,7 +377,10 @@ async function replaceWithBackup(tmpPath, filePath) {
356
377
  }
357
378
  }
358
379
 
359
- async function loadKeytarClient() {
380
+ async function loadKeytarClient({ allowImplicit = false } = {}) {
381
+ if (keytarClientOverrideForTests !== undefined) {
382
+ return keytarClientOverrideForTests;
383
+ }
360
384
  const disableKeyring = String(process.env.SENTINELAYER_DISABLE_KEYRING || "")
361
385
  .trim()
362
386
  .toLowerCase();
@@ -372,7 +396,7 @@ async function loadKeytarClient() {
372
396
  keyringMode === "on" ||
373
397
  keyringMode === "true" ||
374
398
  keyringMode === "1";
375
- if (!enableKeyring) {
399
+ if (!enableKeyring && !allowImplicit) {
376
400
  return null;
377
401
  }
378
402
  try {
@@ -394,6 +418,20 @@ async function loadKeytarClient() {
394
418
  }
395
419
  }
396
420
 
421
+ async function encryptTokenForFileFallback(token, { homeDir } = {}) {
422
+ const key = await loadOrCreateFileKey({ homeDir });
423
+ return encryptToken(token, key);
424
+ }
425
+
426
+ async function attachEncryptedTokenFallback(metadata, token, { homeDir } = {}) {
427
+ const encrypted = await encryptTokenForFileFallback(token, { homeDir });
428
+ metadata.tokenEncrypted = encrypted.tokenEncrypted;
429
+ metadata.tokenIv = encrypted.tokenIv;
430
+ metadata.tokenTag = encrypted.tokenTag;
431
+ metadata.token = null;
432
+ return metadata;
433
+ }
434
+
397
435
  async function readMetadata({ homeDir } = {}) {
398
436
  const filePath = resolveCredentialsFilePath({ homeDir });
399
437
  try {
@@ -480,20 +518,12 @@ async function migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir } = {
480
518
  nextMetadata.storage = "keyring";
481
519
  nextMetadata.keyringService = KEYRING_SERVICE;
482
520
  nextMetadata.keyringAccount = keyringAccount;
483
- const key = await loadOrCreateFileKey({ homeDir });
484
- const encrypted = encryptToken(plaintextToken, key);
485
- nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
486
- nextMetadata.tokenIv = encrypted.tokenIv;
487
- nextMetadata.tokenTag = encrypted.tokenTag;
521
+ await attachEncryptedTokenFallback(nextMetadata, plaintextToken, { homeDir });
488
522
  } else {
489
- const key = await loadOrCreateFileKey({ homeDir });
490
- const encrypted = encryptToken(plaintextToken, key);
491
523
  nextMetadata.storage = "file";
492
524
  nextMetadata.keyringService = KEYRING_SERVICE;
493
525
  nextMetadata.keyringAccount = "";
494
- nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
495
- nextMetadata.tokenIv = encrypted.tokenIv;
496
- nextMetadata.tokenTag = encrypted.tokenTag;
526
+ await attachEncryptedTokenFallback(nextMetadata, plaintextToken, { homeDir });
497
527
  }
498
528
 
499
529
  await writeMetadata(filePath, nextMetadata);
@@ -570,7 +600,7 @@ export async function readStoredSession({ homeDir } = {}) {
570
600
  }
571
601
 
572
602
  if (metadata.storage === "keyring") {
573
- const keytar = await loadKeytarClient();
603
+ const keytar = await loadKeytarClient({ allowImplicit: true });
574
604
  let keyringError = null;
575
605
  if (keytar && metadata.keyringAccount) {
576
606
  try {
@@ -763,17 +793,12 @@ export async function writeStoredSession(
763
793
  nextMetadata.storage = "keyring";
764
794
  nextMetadata.keyringService = KEYRING_SERVICE;
765
795
  nextMetadata.keyringAccount = keyringAccount;
766
- nextMetadata.token = null;
796
+ await attachEncryptedTokenFallback(nextMetadata, normalizedToken, { homeDir });
767
797
  } else {
768
798
  nextMetadata.storage = "file";
769
799
  nextMetadata.keyringService = KEYRING_SERVICE;
770
800
  nextMetadata.keyringAccount = "";
771
- const key = await loadOrCreateFileKey({ homeDir });
772
- const encrypted = encryptToken(normalizedToken, key);
773
- nextMetadata.token = null;
774
- nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
775
- nextMetadata.tokenIv = encrypted.tokenIv;
776
- nextMetadata.tokenTag = encrypted.tokenTag;
801
+ await attachEncryptedTokenFallback(nextMetadata, normalizedToken, { homeDir });
777
802
  }
778
803
 
779
804
  await writeMetadata(filePath, nextMetadata);
@@ -794,7 +819,7 @@ export async function writeStoredSession(
794
819
  export async function clearStoredSession({ homeDir } = {}) {
795
820
  const { filePath, metadata } = await readMetadata({ homeDir });
796
821
  if (metadata && metadata.storage === "keyring") {
797
- const keytar = await loadKeytarClient();
822
+ const keytar = await loadKeytarClient({ allowImplicit: true });
798
823
  if (keytar && metadata.keyringAccount) {
799
824
  await keytar.deletePassword(metadata.keyringService || KEYRING_SERVICE, metadata.keyringAccount);
800
825
  }
@@ -1690,6 +1690,14 @@ export function registerSessionCommand(program) {
1690
1690
  let hydration = null;
1691
1691
  let remoteTail = null;
1692
1692
  if (options.remote) {
1693
+ const authSession = await resolveActiveAuthSession({
1694
+ cwd: targetPath,
1695
+ env: process.env,
1696
+ autoRotate: false,
1697
+ });
1698
+ if (!authSession || !authSession.token) {
1699
+ throw new Error(`Remote session read requires authentication. Run \`${authLoginHint()}\` first.`);
1700
+ }
1693
1701
  hydration = await hydrateSessionFromRemote({
1694
1702
  sessionId: normalizedSessionId,
1695
1703
  targetPath,