@mnemonik/shared 7.99.17 → 7.101.0

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/src/readiness.ts CHANGED
@@ -12,8 +12,6 @@ export type ReadinessConditionKind =
12
12
  | 'scanner_not_verified'
13
13
  | 'hook_not_verified'
14
14
  | 'hooks_missing'
15
- | 'host_grant_unbound'
16
- | 'host_not_connected'
17
15
  | 'scanner_omitted'
18
16
  | 'project_uncovered'
19
17
  | 'host_skipped'
@@ -177,8 +175,6 @@ const stateFor: Record<ReadinessConditionKind, Exclude<ReadinessState, 'READY'>>
177
175
  scanner_not_verified: 'LIMITED',
178
176
  hook_not_verified: 'LIMITED',
179
177
  hooks_missing: 'ACTION_REQUIRED',
180
- host_grant_unbound: 'LIMITED',
181
- host_not_connected: 'LIMITED',
182
178
  scanner_omitted: 'LIMITED',
183
179
  project_uncovered: 'LIMITED',
184
180
  host_skipped: 'LIMITED',
@@ -195,8 +191,6 @@ const rank: Record<ReadinessState, number> = {
195
191
  };
196
192
 
197
193
  function defaultAction(condition: ReadinessCondition): string | undefined {
198
- if (condition.kind === 'host_skipped' && condition.component)
199
- return `Connect it later: mnemonik connect ${condition.component}`;
200
194
  if (condition.kind === 'windows_task_creation_failed')
201
195
  return 'Run mnemonik scanner enable to try again.';
202
196
  return condition.action;
@@ -225,8 +219,7 @@ export function remainingReadinessCount(summary: ReadinessSummary): number {
225
219
  }
226
220
 
227
221
  export function describeReadiness(summary: ReadinessSummary): string {
228
- if (summary.state === 'READY')
229
- return 'Done. Your editors will use Mnemonik on their next session.';
222
+ if (summary.state === 'READY') return 'Done.';
230
223
  const count = remainingReadinessCount(summary);
231
224
  const opening =
232
225
  summary.state === 'FAILED'
@@ -33,11 +33,15 @@ function decodeBase64(text: string, length: number): Buffer {
33
33
  return bytes;
34
34
  }
35
35
 
36
- async function verifyMinisign(path: string, signaturePath: string, identity: string) {
37
- const [message, signatureText] = await Promise.all([
38
- readFile(path),
39
- readFile(signaturePath, 'utf8'),
40
- ]);
36
+ /** Pinned release identity; a downloaded manifest can never replace its own trust anchor. */
37
+ export const RELEASE_MINISIGN_PUBLIC_KEY =
38
+ 'RWSBYwgbjz0qKB4/ToA3dywBiAlQeBjoQpib4OHdjP2nDbpQnOVqpPm0';
39
+
40
+ export function verifyMinisign(
41
+ message: Buffer,
42
+ signatureText: string,
43
+ identity = RELEASE_MINISIGN_PUBLIC_KEY
44
+ ): void {
41
45
  const publicPacket = decodeBase64(identity, 42);
42
46
  const lines = signatureText.trimEnd().split(/\r?\n/);
43
47
  const [untrustedComment, encodedSignature, trustedLine, encodedGlobalSignature] = lines;
@@ -79,6 +83,14 @@ async function verifyMinisign(path: string, signaturePath: string, identity: str
79
83
  throw new Error('unsigned');
80
84
  }
81
85
 
86
+ async function verifyMinisignFile(path: string, signaturePath: string, identity: string) {
87
+ const [message, signatureText] = await Promise.all([
88
+ readFile(path),
89
+ readFile(signaturePath, 'utf8'),
90
+ ]);
91
+ verifyMinisign(message, signatureText, identity);
92
+ }
93
+
82
94
  /** Release tooling supplies the real identity and artifacts. These checks do not sign anything. */
83
95
  export async function verifySigner(path: string, signer: Signer, run = execute): Promise<void> {
84
96
  if (signer.platform === 'darwin' && /^[A-Z0-9]{10}$/.test(signer.identity)) {
@@ -104,7 +116,7 @@ export async function verifySigner(path: string, signer: Signer, run = execute):
104
116
  ]);
105
117
  } else if (signer.platform === 'linux') {
106
118
  try {
107
- await verifyMinisign(path, signer.signature, signer.identity);
119
+ await verifyMinisignFile(path, signer.signature, signer.identity);
108
120
  } catch {
109
121
  throw new Error('unsigned');
110
122
  }
@@ -53,6 +53,7 @@ export function hostAdapterConformance(
53
53
  runtimeRoot,
54
54
  credentialFamily: 'hook-family',
55
55
  runtimeEntry: join(runtimeRoot, '1.0.0', 'dist', 'hook.js'),
56
+ installationId: '11111111-1111-4111-8111-111111111111',
56
57
  };
57
58
  const deps: AdapterDependencies = {
58
59
  target,
@@ -391,55 +392,10 @@ export function hostAdapterConformance(
391
392
  assert.deepEqual(await readFile(configPath('user')), before);
392
393
  }
393
394
  );
394
- check(
395
- 'parses connected listings without inferring account and uses execFile arguments',
396
- async ({ deps, project, target }) => {
397
- let stdout = 'mnemonik: https://api.mnemonik.dev/mcp - ✔ Connected\n';
398
- const calls: unknown[][] = [];
399
- const host = create({
400
- ...deps,
401
- target: { ...target, component: 'mcp' },
402
- execFile: async (...args) => {
403
- if (args[1][0] === '--version') return { stdout: versionOutput, stderr: '' };
404
- calls.push(args);
405
- return { stdout, stderr: '' };
406
- },
407
- });
408
- const inspection = await host.verify();
409
- if (options.name === 'cursor') {
410
- assert.equal(inspection.authenticatedTools, false);
411
- assert.equal(inspection.grant, undefined);
412
- assert.deepEqual(calls, []);
413
- return;
414
- }
415
- assert.equal(inspection.authenticatedTools, true);
416
- assert.equal(inspection.grant, undefined);
417
- assert.deepEqual(calls[0], [
418
- options.name === 'claude-code'
419
- ? 'claude'
420
- : options.name === 'cursor'
421
- ? 'agent'
422
- : options.name,
423
- ['mcp', 'list'],
424
- { env: deps.env, cwd: project, timeout: 5000, maxBuffer: 64 * 1024, windowsHide: true },
425
- ]);
426
- for (const listing of [
427
- 'mnemonik: not connected',
428
- 'foreign: ✔ Connected',
429
- 'mnemonik: enabled OAuth',
430
- 'mnemonik-foreign: Connected',
431
- 'mnemonik: warning: was previously connected',
432
- 'unrecognized format',
433
- ]) {
434
- stdout = listing;
435
- assert.equal((await host.verify()).authenticatedTools, false);
436
- }
437
- }
438
- );
439
395
  if (options.mcpConfig)
440
396
  for (const scope of options.scopes)
441
397
  check(
442
- `${scope}: MCP URL only, foreign server preserved, staged lifecycle`,
398
+ `${scope}: MCP declaration has the correct machine header and preserves foreign fields`,
443
399
  async ({ root, project, deps, target: base }) => {
444
400
  const target = { ...base, component: 'mcp' as const, scope };
445
401
  const adapter = create({ ...deps, target });
@@ -458,14 +414,14 @@ export function hostAdapterConformance(
458
414
  const raw = plan.changes[0]?.content.toString();
459
415
  assert.ok(raw);
460
416
  assert.ok(raw.includes('https://api.mnemonik.dev/mcp'));
461
- const sessionHeader = 'headers = { "x-mcp-session-id" = "{{session_id}}" }\n';
417
+ const sessionHeader = /headers\s*=.*"x-mcp-session-id"\s*=\s*"\{\{session_id\}\}"/u;
462
418
  const withoutSessionHeader =
463
- options.name === 'grok' && config.toml ? raw.replace(sessionHeader, '') : raw;
464
- if (options.name === 'grok') assert.ok(raw.includes(sessionHeader));
465
- assert.doesNotMatch(
466
- withoutSessionHeader,
467
- /token|secret|headers|credential|bearer|runtime|client.?id/i
468
- );
419
+ options.name === 'grok' && config.toml
420
+ ? raw.replace(/,?\s*"x-mcp-session-id"\s*=\s*"\{\{session_id\}\}"/u, '')
421
+ : raw;
422
+ if (options.name === 'grok') assert.match(raw, sessionHeader);
423
+ if (scope === 'user') assert.match(withoutSessionHeader, /x-mnemonik-installation-id/);
424
+ else assert.doesNotMatch(withoutSessionHeader, /x-mnemonik-installation-id/);
469
425
  if (config.toml) assert.ok(raw.startsWith(original));
470
426
  else {
471
427
  const parsed = JSON.parse(raw);
@@ -473,6 +429,13 @@ export function hostAdapterConformance(
473
429
  assert.deepEqual(parsed.mcpServers.mnemonik, {
474
430
  ...(config.type ? { type: config.type } : {}),
475
431
  url: 'https://api.mnemonik.dev/mcp',
432
+ ...(scope === 'user'
433
+ ? {
434
+ headers: {
435
+ 'x-mnemonik-installation-id': '11111111-1111-4111-8111-111111111111',
436
+ },
437
+ }
438
+ : {}),
476
439
  });
477
440
  }
478
441
  const staged: FileChange[] = [];
@@ -487,6 +450,52 @@ export function hostAdapterConformance(
487
450
  await writeChanges(staged);
488
451
  assert.equal((await adapter.inspect()).declarationPresent, true);
489
452
  assert.deepEqual((await adapter.plan()).changes, plan.changes);
453
+
454
+ if (scope === 'user') {
455
+ const installed = await readFile(path, 'utf8');
456
+ if (config.toml)
457
+ await writeFile(
458
+ path,
459
+ installed
460
+ .replace(/,?\s*"x-mnemonik-installation-id"\s*=\s*"[^"]*"/u, '')
461
+ .replace(/\{\s*,/u, '{')
462
+ .replace(/,\s*\}/u, ' }')
463
+ .replace(
464
+ 'url = "https://api.mnemonik.dev/mcp"\n',
465
+ 'url = "https://api.mnemonik.dev/mcp"\ncustom = "keep exactly"\n'
466
+ )
467
+ );
468
+ else {
469
+ const missing = JSON.parse(installed);
470
+ missing.mcpServers.mnemonik.custom = 'keep exactly';
471
+ missing.mcpServers.mnemonik.headers['x-foreign'] = 'keep exactly';
472
+ delete missing.mcpServers.mnemonik.headers['x-mnemonik-installation-id'];
473
+ await writeFile(path, JSON.stringify(missing, null, 2) + '\n');
474
+ }
475
+ const missingHeader = await readFile(path, 'utf8');
476
+ staged.length = 0;
477
+ await adapter.repair(writer, target);
478
+ await writeChanges(staged);
479
+ const repaired = await readFile(path, 'utf8');
480
+ assert.match(repaired, /x-mnemonik-installation-id/);
481
+ assert.match(repaired, /keep exactly/);
482
+ if (options.name === 'grok') assert.match(repaired, /x-mcp-session-id/);
483
+ if (config.toml) {
484
+ const withoutManagedHeader = repaired
485
+ .replace(
486
+ /(?:,\s*)?(?:["']x-mnemonik-installation-id["']|x-mnemonik-installation-id)\s*=\s*["'][^"']*["']\s*,?/u,
487
+ ''
488
+ )
489
+ .replace(/\{\s*,/u, '{')
490
+ .replace(/,\s*\}/u, ' }');
491
+ assert.equal(withoutManagedHeader, missingHeader);
492
+ } else {
493
+ const withoutManagedHeader = JSON.parse(repaired);
494
+ delete withoutManagedHeader.mcpServers.mnemonik.headers['x-mnemonik-installation-id'];
495
+ assert.equal(JSON.stringify(withoutManagedHeader, null, 2) + '\n', missingHeader);
496
+ }
497
+ }
498
+
490
499
  staged.length = 0;
491
500
  await adapter.uninstall(writer, target);
492
501
  await writeChanges(staged);
@@ -13,7 +13,6 @@ const cases: Array<[ReadinessCondition, ReadinessState]> = [
13
13
  [{ kind: 'host_trust_pending', reason: 'Approve Cursor.' }, 'ACTION_REQUIRED'],
14
14
  [{ kind: 'vendor_policy_pending', reason: 'Allow the extension.' }, 'ACTION_REQUIRED'],
15
15
  [{ kind: 'hooks_missing', reason: 'Hook declaration is missing.' }, 'ACTION_REQUIRED'],
16
- [{ kind: 'host_not_connected', reason: 'Codex has not connected.' }, 'LIMITED'],
17
16
  [{ kind: 'scanner_omitted', reason: 'Scanner was omitted.' }, 'LIMITED'],
18
17
  [{ kind: 'project_uncovered', reason: 'Project is outside approved roots.' }, 'LIMITED'],
19
18
  [{ kind: 'post_commit_upload_failed', reason: 'Result upload failed.' }, 'FAILED'],
@@ -100,7 +99,7 @@ test('the CLI sentence names the concrete reason and action', () => {
100
99
  },
101
100
  ])
102
101
  ),
103
- 'Done, with one thing left. Cursor was skipped. Connect it later: mnemonik connect cursor'
102
+ 'Done, with one thing left. Cursor was skipped.'
104
103
  );
105
104
  });
106
105