@mintlify/cli 4.0.1734 → 4.0.1736

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.
@@ -54,7 +54,7 @@ afterEach(() => vi.unstubAllGlobals());
54
54
 
55
55
  describe('claim command', () => {
56
56
  it('completes the existing claim, saves the new identity, and signs up with a fresh token', async () => {
57
- expect(await claimDeployment(options, vi.fn())).toContain('successfully');
57
+ expect(await claimDeployment(options)).toContain('successfully');
58
58
  expect(fetchMock).toHaveBeenCalledWith(
59
59
  `${issuer}/agent/identity/claim/complete`,
60
60
  expect.objectContaining({
@@ -83,11 +83,11 @@ describe('claim command', () => {
83
83
  fetchMock.mockImplementation((url, init) =>
84
84
  url.endsWith('/signup') ? response({ code: 'unavailable' }, 503) : implementation(url, init)
85
85
  );
86
- await expect(claimDeployment(options, vi.fn())).rejects.toThrow('503');
86
+ await expect(claimDeployment(options)).rejects.toThrow('503');
87
87
  const saved = vi.mocked(saveAgentCredentials).mock.calls[0]![2];
88
88
  vi.mocked(loadAgentCredentials).mockResolvedValue(saved);
89
89
  fetchMock.mockClear().mockImplementation(implementation);
90
- expect(await claimDeployment(options, vi.fn())).toContain('successfully');
90
+ expect(await claimDeployment(options)).toContain('successfully');
91
91
  expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/complete'))).toBe(false);
92
92
  });
93
93
  it('does not save or sign up after an invalid code', async () => {
@@ -97,25 +97,22 @@ describe('claim command', () => {
97
97
  ? response({ code: 'invalid_user_code' }, 400)
98
98
  : implementation(url, init)
99
99
  );
100
- await expect(claimDeployment(options, vi.fn())).rejects.toThrow('invalid_user_code');
100
+ await expect(claimDeployment(options)).rejects.toThrow('invalid_user_code');
101
101
  expect(saveAgentCredentials).not.toHaveBeenCalled();
102
102
  expect(trackEvent).not.toHaveBeenCalled();
103
103
  expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/signup'))).toBe(false);
104
104
  });
105
- it('warns on storage failure and continues signup without exposing secrets', async () => {
106
- vi.mocked(saveAgentCredentials).mockRejectedValue(new Error('keyring unavailable'));
107
- const warn = vi.fn();
108
- const output = await claimDeployment(options, warn);
109
- expect(output).toContain('successfully');
110
- expect(warn).toHaveBeenCalled();
111
- expect(JSON.stringify([output, warn.mock.calls])).not.toContain('secret-claim');
112
- expect(JSON.stringify([output, warn.mock.calls])).not.toContain('claimed-assertion');
105
+ it('fails before signup when credentials cannot be saved', async () => {
106
+ vi.mocked(saveAgentCredentials).mockRejectedValue(new Error('disk full'));
107
+ await expect(claimDeployment(options)).rejects.toThrow('disk full');
108
+ expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/signup'))).toBe(false);
113
109
  });
114
110
  it('creates only a claim link for --email', async () => {
115
- const output = await claimDeployment(
116
- { apiUrl, directory: '/docs', email: 'person@example.com' },
117
- vi.fn()
118
- );
111
+ const output = await claimDeployment({
112
+ apiUrl,
113
+ directory: '/docs',
114
+ email: 'person@example.com',
115
+ });
119
116
  expect(new URL(output.match(/https?:\/\/\S+/)![0]).searchParams.get('registration')).toBe(
120
117
  'reg'
121
118
  );
@@ -128,15 +125,13 @@ describe('claim command', () => {
128
125
  });
129
126
  it('fails before network calls when credentials are missing', async () => {
130
127
  vi.mocked(loadAgentCredentials).mockResolvedValue(null);
131
- await expect(claimDeployment(options, vi.fn())).rejects.toThrow('No saved agent registration');
128
+ await expect(claimDeployment(options)).rejects.toThrow('No saved agent registration');
132
129
  expect(fetchMock).not.toHaveBeenCalled();
133
130
  });
134
131
  it.each([{ code: undefined }, { code: ' ', email: undefined }, { email: 'person@example.com' }])(
135
132
  'rejects missing or conflicting arguments: %o',
136
133
  async (override) => {
137
- await expect(claimDeployment({ ...options, ...override }, vi.fn())).rejects.toThrow(
138
- 'Provide either'
139
- );
134
+ await expect(claimDeployment({ ...options, ...override })).rejects.toThrow('Provide either');
140
135
  expect(fetchMock).not.toHaveBeenCalled();
141
136
  }
142
137
  );
@@ -145,7 +140,7 @@ describe('claim command', () => {
145
140
  ...original,
146
141
  issuer: 'https://other.example.com',
147
142
  });
148
- await expect(claimDeployment(options, vi.fn())).rejects.toThrow('another issuer');
143
+ await expect(claimDeployment(options)).rejects.toThrow('another issuer');
149
144
  expect(fetchMock.mock.calls).toHaveLength(2);
150
145
  });
151
146
  it('does not report success if signup returns an anonymous identity', async () => {
@@ -155,6 +150,6 @@ describe('claim command', () => {
155
150
  ? response({ orgId: 'org', claimed: false })
156
151
  : implementation(url, init)
157
152
  );
158
- await expect(claimDeployment(options, vi.fn())).rejects.toThrow('unexpected response');
153
+ await expect(claimDeployment(options)).rejects.toThrow('unexpected response');
159
154
  });
160
155
  });
@@ -1,8 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
1
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
5
 
3
6
  const entries = new Map<string, string>();
4
- const keytar = vi.hoisted(() => ({ getPassword: vi.fn(), setPassword: vi.fn() }));
7
+ const keytar = vi.hoisted(() => ({
8
+ getPassword: vi.fn(),
9
+ setPassword: vi.fn(),
10
+ deletePassword: vi.fn(),
11
+ }));
5
12
  vi.mock('../src/keyring.js', () => ({ getKeytar: async () => keytar }));
13
+ const configDir = vi.hoisted(() => ({ path: '' }));
14
+ vi.mock('../src/constants.js', () => ({
15
+ get CONFIG_DIR() {
16
+ return configDir.path;
17
+ },
18
+ }));
6
19
 
7
20
  import { loadAgentCredentials, saveAgentCredentials } from '../src/agent-credentials.js';
8
21
 
@@ -17,12 +30,16 @@ describe('agent credentials', () => {
17
30
  beforeEach(() => {
18
31
  entries.clear();
19
32
  vi.resetAllMocks();
33
+ configDir.path = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-agent-'));
20
34
  keytar.getPassword.mockImplementation(
21
35
  async (service, account) => entries.get(`${service}/${account}`) ?? null
22
36
  );
23
37
  keytar.setPassword.mockImplementation(async (service, account, value) => {
24
38
  entries.set(`${service}/${account}`, value);
25
39
  });
40
+ keytar.deletePassword.mockImplementation(async (service, account) =>
41
+ entries.delete(`${service}/${account}`)
42
+ );
26
43
  });
27
44
 
28
45
  it('isolates projects and environments from each other and human credentials', async () => {
@@ -45,12 +62,82 @@ describe('agent credentials', () => {
45
62
  expect(entries.has('mintlify-agent/https://api.example.com:reg_1')).toBe(true);
46
63
  });
47
64
 
48
- it('does not switch the project pointer when storing the credential fails', async () => {
65
+ it('falls back to a local file when the keyring cannot be written or read', async () => {
66
+ keytar.setPassword.mockRejectedValue(new Error('locked'));
67
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
68
+ expect(entries.size).toBe(0);
69
+ const file = path.join(configDir.path, 'agent-credentials.json');
70
+ if (process.platform !== 'win32') expect(fs.statSync(file).mode & 0o777).toBe(0o600);
71
+ keytar.getPassword.mockRejectedValue(new Error('locked'));
72
+ expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
73
+ });
74
+
75
+ it('removes stale fallback entries once the keyring is writable again', async () => {
49
76
  keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
77
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
78
+ await saveAgentCredentials('https://api.example.com', '/docs', {
79
+ ...credentials,
80
+ assertion: 'next',
81
+ });
82
+ expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.assertion).toBe(
83
+ 'next'
84
+ );
85
+ const file = path.join(configDir.path, 'agent-credentials.json');
86
+ expect(JSON.parse(fs.readFileSync(file, 'utf-8'))).toEqual({});
87
+ });
88
+
89
+ it('reads the keyring before the fallback file', async () => {
90
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
91
+ fs.writeFileSync(
92
+ path.join(configDir.path, 'agent-credentials.json'),
93
+ JSON.stringify({
94
+ 'https://api.example.com:reg_1': JSON.stringify({ ...credentials, assertion: 'stale' }),
95
+ })
96
+ );
97
+ expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
98
+ });
99
+
100
+ it('ignores a corrupt fallback file when the keyring has the credentials', async () => {
101
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
102
+ fs.writeFileSync(path.join(configDir.path, 'agent-credentials.json'), '{');
103
+ expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
104
+ });
105
+
106
+ it('drops the stale keyring entry when a write falls back to the file', async () => {
107
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
108
+ keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
109
+ await saveAgentCredentials('https://api.example.com', '/docs', {
110
+ ...credentials,
111
+ assertion: 'next',
112
+ });
113
+ expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.assertion).toBe(
114
+ 'next'
115
+ );
116
+ });
117
+
118
+ it('succeeds when the keyring write works but fallback cleanup fails', async () => {
119
+ keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
120
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
121
+ fs.chmodSync(configDir.path, 0o500);
122
+ await expect(
123
+ saveAgentCredentials('https://api.example.com', '/docs', {
124
+ ...credentials,
125
+ assertion: 'next',
126
+ })
127
+ ).resolves.toBeUndefined();
128
+ fs.chmodSync(configDir.path, 0o700);
129
+ expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.assertion).toBe(
130
+ 'next'
131
+ );
132
+ });
133
+
134
+ it('fails when neither the keyring nor the local file can be written', async () => {
135
+ keytar.setPassword.mockRejectedValue(new Error('locked'));
136
+ configDir.path = path.join(configDir.path, 'not-a-directory');
137
+ fs.writeFileSync(configDir.path, '');
50
138
  await expect(
51
139
  saveAgentCredentials('https://api.example.com', '/docs', credentials)
52
- ).rejects.toThrow('locked');
53
- expect(keytar.setPassword).toHaveBeenCalledTimes(1);
140
+ ).rejects.toThrow();
54
141
  });
55
142
 
56
143
  it('preserves read failures instead of treating them as an absent registration', async () => {
@@ -82,12 +82,10 @@ describe('mint deploy', () => {
82
82
  const result = await prepareAgentDeployment({ ...options, apiUrl }, warn);
83
83
  expect(formatDeployment(result, apiUrl)).toContain(`Site: ${siteUrl}`);
84
84
  });
85
- it('continues provisioning after keyring save failure', async () => {
86
- vi.mocked(storage.saveAgentCredentials).mockRejectedValue(new Error('locked'));
87
- const result = await prepareAgentDeployment(options, warn);
88
- expect(result.status).toBe('ready');
89
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('Continuing for this run'));
90
- expect(formatDeployment(result, options.apiUrl)).toContain('https://allocated.mintlify.site');
85
+ it('fails before provisioning when credentials cannot be saved', async () => {
86
+ vi.mocked(storage.saveAgentCredentials).mockRejectedValue(new Error('disk full'));
87
+ await expect(prepareAgentDeployment(options, warn)).rejects.toThrow('disk full');
88
+ expect(deployment.provisionAgentSite).not.toHaveBeenCalled();
91
89
  });
92
90
  it('persists the exchanged access token for later commands', async () => {
93
91
  await prepareAgentDeployment(options, warn);
@@ -45,8 +45,8 @@ const completedClaimSchema = z.object({
45
45
  identity: agentIdentityAssertionSchema,
46
46
  });
47
47
  const signupSchema = z.object({ orgId: z.string().min(1), claimed: z.literal(true) });
48
- export function claimDeployment(options_1, warn_1) {
49
- return __awaiter(this, arguments, void 0, function* (options, warn, progress = () => { }) {
48
+ export function claimDeployment(options_1) {
49
+ return __awaiter(this, arguments, void 0, function* (options, progress = () => { }) {
50
50
  var _a, _b;
51
51
  const { apiUrl, directory } = options;
52
52
  const code = (_a = options.code) === null || _a === void 0 ? void 0 : _a.trim();
@@ -66,14 +66,7 @@ export function claimDeployment(options_1, warn_1) {
66
66
  if (new URL(discovery.issuer).origin !== new URL(credentials.issuer).origin) {
67
67
  throw new Error('Saved agent credentials belong to another issuer.');
68
68
  }
69
- const save = (value) => __awaiter(this, void 0, void 0, function* () {
70
- try {
71
- yield saveAgentCredentials(apiUrl, directory, value);
72
- }
73
- catch (_a) {
74
- warn('Could not save claimed credentials to the keyring. Continuing for this run; a retry may not recover the completed claim.');
75
- }
76
- });
69
+ const save = (value) => saveAgentCredentials(apiUrl, directory, value);
77
70
  if (!credentials.claimVerified) {
78
71
  progress('Completing the WorkOS claim...');
79
72
  const completed = yield agentResponse(yield agentRequest(`${discovery.agent_auth.claim_endpoint.replace(/\/$/, '')}/complete`, {
@@ -7,11 +7,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ var __rest = (this && this.__rest) || function (s, e) {
11
+ var t = {};
12
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
13
+ t[p] = s[p];
14
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
15
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
16
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
17
+ t[p[i]] = s[p[i]];
18
+ }
19
+ return t;
20
+ };
10
21
  import { createHash } from 'node:crypto';
22
+ import fs from 'node:fs/promises';
11
23
  import path from 'node:path';
12
24
  import { z } from 'zod';
25
+ import { CONFIG_DIR } from './constants.js';
13
26
  import { getKeytar } from './keyring.js';
14
27
  const service = 'mintlify-agent';
28
+ const fallbackFile = () => path.join(CONFIG_DIR, 'agent-credentials.json');
15
29
  export const agentCredentialsSchema = z.object({
16
30
  registrationId: z.string().min(1),
17
31
  issuer: z.url(),
@@ -29,13 +43,69 @@ function projectAccount(apiUrl, directory) {
29
43
  function registrationAccount(apiUrl, registrationId) {
30
44
  return `${new URL(apiUrl).origin}:${registrationId}`;
31
45
  }
46
+ function readFallback() {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ try {
49
+ return z
50
+ .record(z.string(), z.string())
51
+ .parse(JSON.parse(yield fs.readFile(fallbackFile(), 'utf-8')));
52
+ }
53
+ catch (_a) {
54
+ return {};
55
+ }
56
+ });
57
+ }
58
+ function writeFallback(entries) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ yield fs.mkdir(CONFIG_DIR, { recursive: true });
61
+ const temp = `${fallbackFile()}.${process.pid}.tmp`;
62
+ yield fs.writeFile(temp, JSON.stringify(entries, null, 2), { mode: 0o600 });
63
+ yield fs.chmod(temp, 0o600);
64
+ yield fs.rename(temp, fallbackFile());
65
+ });
66
+ }
67
+ function getSecret(account) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ let keyringError;
70
+ try {
71
+ const value = yield (yield getKeytar()).getPassword(service, account);
72
+ if (value)
73
+ return value;
74
+ }
75
+ catch (error) {
76
+ keyringError = error;
77
+ }
78
+ const value = (yield readFallback())[account];
79
+ if (value)
80
+ return value;
81
+ if (keyringError)
82
+ throw keyringError;
83
+ return null;
84
+ });
85
+ }
86
+ function setSecret(account, value) {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ try {
89
+ yield (yield getKeytar()).setPassword(service, account, value);
90
+ }
91
+ catch (_a) {
92
+ yield writeFallback(Object.assign(Object.assign({}, (yield readFallback())), { [account]: value }));
93
+ yield getKeytar()
94
+ .then((keytar) => keytar.deletePassword(service, account))
95
+ .catch(() => { });
96
+ return;
97
+ }
98
+ const _b = yield readFallback(), _c = account, stale = _b[_c], rest = __rest(_b, [typeof _c === "symbol" ? _c : _c + ""]);
99
+ if (stale)
100
+ yield writeFallback(rest).catch(() => { });
101
+ });
102
+ }
32
103
  export function loadAgentCredentials(apiUrl, directory) {
33
104
  return __awaiter(this, void 0, void 0, function* () {
34
- const keytar = yield getKeytar();
35
- const registrationId = yield keytar.getPassword(service, projectAccount(apiUrl, directory));
105
+ const registrationId = yield getSecret(projectAccount(apiUrl, directory));
36
106
  if (!registrationId)
37
107
  return null;
38
- const value = yield keytar.getPassword(service, registrationAccount(apiUrl, registrationId));
108
+ const value = yield getSecret(registrationAccount(apiUrl, registrationId));
39
109
  if (!value)
40
110
  throw new Error('Saved agent credentials are missing. Use --new to create another site.');
41
111
  const credentials = agentCredentialsSchema.parse(JSON.parse(value));
@@ -48,8 +118,7 @@ export function loadAgentCredentials(apiUrl, directory) {
48
118
  export function saveAgentCredentials(apiUrl, directory, credentials) {
49
119
  return __awaiter(this, void 0, void 0, function* () {
50
120
  const value = agentCredentialsSchema.parse(credentials);
51
- const keytar = yield getKeytar();
52
- yield keytar.setPassword(service, registrationAccount(apiUrl, value.registrationId), JSON.stringify(value));
53
- yield keytar.setPassword(service, projectAccount(apiUrl, directory), value.registrationId);
121
+ yield setSecret(registrationAccount(apiUrl, value.registrationId), JSON.stringify(value));
122
+ yield setSecret(projectAccount(apiUrl, directory), value.registrationId);
54
123
  });
55
124
  }
package/bin/cli.js CHANGED
@@ -115,7 +115,7 @@ export const cli = ({ packageName = 'mint' }) => {
115
115
  .example('$0 claim --code ABCDEFGH', 'Finish the claim with the code from the user')
116
116
  .epilog('Run from the same directory as mint deploy, or pass --directory. Ask the user to open the claim link and return the code. The CLI reads saved credentials internally; do not read or print keychain secrets. Retry the same command if Mintlify signup fails.'), (argv) => __awaiter(void 0, void 0, void 0, function* () {
117
117
  try {
118
- const message = yield claimDeployment({ apiUrl: API_URL, directory: argv.directory, code: argv.code, email: argv.email }, (message) => process.stderr.write(`Warning: ${message}\n`), (message) => process.stderr.write(`${message}\n`));
118
+ const message = yield claimDeployment({ apiUrl: API_URL, directory: argv.directory, code: argv.code, email: argv.email }, (message) => process.stderr.write(`${message}\n`));
119
119
  process.stdout.write(`${message}\n`);
120
120
  yield terminate(0);
121
121
  }
package/bin/deploy.js CHANGED
@@ -33,14 +33,7 @@ export function prepareAgentDeployment(options_1, warn_1) {
33
33
  if (!subdomain)
34
34
  throw new Error('Choose a site name with --subdomain <name>.');
35
35
  const discovery = yield discoverAgentAuth(apiUrl);
36
- const save = (credentials) => __awaiter(this, void 0, void 0, function* () {
37
- try {
38
- yield saveAgentCredentials(apiUrl, directory, credentials);
39
- }
40
- catch (_a) {
41
- warn('Could not save agent credentials to the keyring. Continuing for this run; later commands may not recover access.');
42
- }
43
- });
36
+ const save = (credentials) => saveAgentCredentials(apiUrl, directory, credentials);
44
37
  for (let attempt = 0; attempt < 2; attempt++) {
45
38
  const credentials = saved !== null && saved !== void 0 ? saved : (yield registerAgent(discovery));
46
39
  void trackEvent('agent_signup.discovered', { registration_id: credentials.registrationId });