@mintlify/cli 4.0.1701 → 4.0.1703

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.
@@ -0,0 +1,114 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ discoverAgentAuth,
5
+ exchangeAgentToken,
6
+ getAgentIdentity,
7
+ registerAgent,
8
+ } from '../src/agent-auth.js';
9
+ import { agentRequest, AgentApiError } from '../src/agent-http.js';
10
+
11
+ const issuer = 'https://auth.example.com';
12
+ const discovery = {
13
+ issuer,
14
+ token_endpoint: `${issuer}/oauth2/token`,
15
+ agent_auth: {
16
+ identity_endpoint: `${issuer}/agent/identity`,
17
+ claim_endpoint: `${issuer}/agent/identity/claim`,
18
+ identity_types_supported: ['anonymous'],
19
+ },
20
+ };
21
+ const credentials = {
22
+ registrationId: 'reg_1',
23
+ issuer,
24
+ assertion: 'assertion-secret',
25
+ claimToken: 'claim-secret',
26
+ };
27
+ function respond(body: unknown, status = 200) {
28
+ return new Response(JSON.stringify(body), { status });
29
+ }
30
+ afterEach(() => vi.unstubAllGlobals());
31
+
32
+ describe('agent authentication', () => {
33
+ it('preserves caller cancellation alongside the request timeout', async () => {
34
+ const controller = new AbortController();
35
+ let requestSignal: AbortSignal | undefined;
36
+ vi.stubGlobal(
37
+ 'fetch',
38
+ vi.fn(async (_url, init: RequestInit) => {
39
+ requestSignal = init.signal ?? undefined;
40
+ return respond({});
41
+ })
42
+ );
43
+ await agentRequest('https://api.example.com', { signal: controller.signal });
44
+ expect(requestSignal?.aborted).toBe(false);
45
+ controller.abort();
46
+ expect(requestSignal?.aborted).toBe(true);
47
+ });
48
+ it('discovers endpoints from Mintlify and validates their origin', async () => {
49
+ const request = vi
50
+ .fn()
51
+ .mockResolvedValueOnce(respond({ authorization_servers: [issuer] }))
52
+ .mockResolvedValueOnce(respond(discovery));
53
+ vi.stubGlobal('fetch', request);
54
+ expect(await discoverAgentAuth('https://api.example.com')).toEqual(discovery);
55
+ expect(request.mock.calls[0]?.[0]).toBe(
56
+ 'https://api.example.com/api/agent/.well-known/oauth-protected-resource'
57
+ );
58
+ });
59
+ it('does not send credentials to endpoints on a different origin', async () => {
60
+ vi.stubGlobal(
61
+ 'fetch',
62
+ vi
63
+ .fn()
64
+ .mockResolvedValueOnce(respond({ authorization_servers: [issuer] }))
65
+ .mockResolvedValueOnce(
66
+ respond({ ...discovery, token_endpoint: 'https://other.example.com/token' })
67
+ )
68
+ );
69
+ await expect(discoverAgentAuth('https://api.example.com')).rejects.toThrow('do not match');
70
+ });
71
+ it('parses the hosted registration response and exchanges without resource', async () => {
72
+ const request = vi
73
+ .fn()
74
+ .mockResolvedValueOnce(
75
+ respond({
76
+ id: 'reg_1',
77
+ identity: { assertion: credentials.assertion },
78
+ claim: { token: credentials.claimToken },
79
+ })
80
+ )
81
+ .mockResolvedValueOnce(
82
+ respond({ access_token: 'access', token_type: 'Bearer', expires_in: 300 })
83
+ );
84
+ vi.stubGlobal('fetch', request);
85
+ expect(await registerAgent(discovery)).toEqual(credentials);
86
+ expect((await exchangeAgentToken(discovery, credentials)).accessToken).toBe('access');
87
+ const body = request.mock.calls[1]?.[1].body;
88
+ expect(body.get('assertion')).toBe(credentials.assertion);
89
+ expect(body.has('resource')).toBe(false);
90
+ });
91
+ it('sends the agent token without reading human credentials', async () => {
92
+ const request = vi
93
+ .fn()
94
+ .mockResolvedValue(respond({ registrationId: 'reg_1', identityType: 'anonymous' }));
95
+ vi.stubGlobal('fetch', request);
96
+ await getAgentIdentity('https://api.example.com', 'agent-token');
97
+ expect(request.mock.calls[0]?.[1].headers.Authorization).toBe('Bearer agent-token');
98
+ });
99
+ it('does not register again on outages or print upstream secrets', async () => {
100
+ const request = vi
101
+ .fn()
102
+ .mockResolvedValue(respond({ error: 'unavailable', message: credentials.assertion }, 503));
103
+ vi.stubGlobal('fetch', request);
104
+ await expect(exchangeAgentToken(discovery, credentials)).rejects.toThrow(AgentApiError);
105
+ expect(request).toHaveBeenCalledTimes(1);
106
+ });
107
+ it('rejects malformed successful responses without exposing them', async () => {
108
+ vi.stubGlobal(
109
+ 'fetch',
110
+ vi.fn().mockResolvedValue(respond({ assertion: credentials.assertion }))
111
+ );
112
+ await expect(registerAgent(discovery)).rejects.toThrow('unexpected response');
113
+ });
114
+ });
@@ -0,0 +1,62 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const entries = new Map<string, string>();
4
+ const keytar = vi.hoisted(() => ({ getPassword: vi.fn(), setPassword: vi.fn() }));
5
+ vi.mock('../src/keyring.js', () => ({ getKeytar: async () => keytar }));
6
+
7
+ import { loadAgentCredentials, saveAgentCredentials } from '../src/agent-credentials.js';
8
+
9
+ const credentials = {
10
+ registrationId: 'reg_1',
11
+ issuer: 'https://auth.example.com',
12
+ assertion: 'assertion',
13
+ claimToken: 'claim',
14
+ };
15
+
16
+ describe('agent credentials', () => {
17
+ beforeEach(() => {
18
+ entries.clear();
19
+ vi.resetAllMocks();
20
+ keytar.getPassword.mockImplementation(
21
+ async (service, account) => entries.get(`${service}/${account}`) ?? null
22
+ );
23
+ keytar.setPassword.mockImplementation(async (service, account, value) => {
24
+ entries.set(`${service}/${account}`, value);
25
+ });
26
+ });
27
+
28
+ it('isolates projects and environments from each other and human credentials', async () => {
29
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
30
+ expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
31
+ expect(await loadAgentCredentials('https://dev.example.com', '/docs')).toBeNull();
32
+ expect(await loadAgentCredentials('https://api.example.com', '/other')).toBeNull();
33
+ expect([...entries.keys()].every((key) => key.startsWith('mintlify-agent/'))).toBe(true);
34
+ });
35
+
36
+ it('retains old registrations when the current project starts a new site', async () => {
37
+ await saveAgentCredentials('https://api.example.com', '/docs', credentials);
38
+ await saveAgentCredentials('https://api.example.com', '/docs', {
39
+ ...credentials,
40
+ registrationId: 'reg_2',
41
+ });
42
+ expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.registrationId).toBe(
43
+ 'reg_2'
44
+ );
45
+ expect(entries.has('mintlify-agent/https://api.example.com:reg_1')).toBe(true);
46
+ });
47
+
48
+ it('does not switch the project pointer when storing the credential fails', async () => {
49
+ keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
50
+ await expect(
51
+ saveAgentCredentials('https://api.example.com', '/docs', credentials)
52
+ ).rejects.toThrow('locked');
53
+ expect(keytar.setPassword).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it('preserves read failures instead of treating them as an absent registration', async () => {
57
+ keytar.getPassword.mockRejectedValueOnce(new Error('locked'));
58
+ await expect(loadAgentCredentials('https://api.example.com', '/docs')).rejects.toThrow(
59
+ 'locked'
60
+ );
61
+ });
62
+ });
@@ -0,0 +1,80 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getAgentWriteToken, provisionAgentSite } from '../src/agent-deployment.js';
4
+
5
+ vi.mock('node:timers/promises', () => ({ setTimeout: vi.fn().mockResolvedValue(undefined) }));
6
+ const ready = {
7
+ status: 'ready',
8
+ deploymentId: 'deployment',
9
+ subdomain: 'actual-name',
10
+ remote: 'https://git.example.com/repo.git',
11
+ branch: 'main',
12
+ expiresAt: '2026-12-01T00:00:00.000Z',
13
+ };
14
+ const existing = { ...ready, code: 'deployment_already_exists' };
15
+ const provision = () =>
16
+ provisionAgentSite('https://api.example.com', 'access', {
17
+ subdomain: 'requested',
18
+ orgName: 'Docs',
19
+ });
20
+ function response(body: unknown, status: number) {
21
+ return new Response(JSON.stringify(body), { status });
22
+ }
23
+ afterEach(() => vi.unstubAllGlobals());
24
+
25
+ describe('agent deployment API', () => {
26
+ it('sends root content layout and uses the allocated subdomain', async () => {
27
+ const request = vi.fn().mockResolvedValue(response(ready, 201));
28
+ vi.stubGlobal('fetch', request);
29
+ expect(await provision()).toEqual({ ...ready, created: true });
30
+ expect(JSON.parse(request.mock.calls[0]?.[1].body)).toEqual({
31
+ subdomain: 'requested',
32
+ orgName: 'Docs',
33
+ docsPath: '',
34
+ });
35
+ });
36
+ it('returns an existing site as an update, not a new creation', async () => {
37
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response(existing, 409)));
38
+ expect(await provision()).toEqual({ ...existing, created: false });
39
+ });
40
+ it('retries provisioning even when no deployment ID exists yet', async () => {
41
+ const request = vi
42
+ .fn()
43
+ .mockResolvedValueOnce(response({ status: 'provisioning', code: existing.code }, 409))
44
+ .mockResolvedValueOnce(response(existing, 409));
45
+ vi.stubGlobal('fetch', request);
46
+ expect((await provision()).status).toBe('ready');
47
+ expect(request).toHaveBeenCalledTimes(2);
48
+ });
49
+ it('bounds provisioning retries', async () => {
50
+ const request = vi
51
+ .fn()
52
+ .mockImplementation(async () =>
53
+ response({ status: 'provisioning', code: existing.code }, 409)
54
+ );
55
+ vi.stubGlobal('fetch', request);
56
+ await expect(provision()).rejects.toThrow('still provisioning');
57
+ expect(request).toHaveBeenCalledTimes(4);
58
+ });
59
+ it('distinguishes confirmed expiry from upstream failure', async () => {
60
+ const request = vi
61
+ .fn()
62
+ .mockResolvedValueOnce(response({ ...existing, status: 'expired' }, 409))
63
+ .mockResolvedValueOnce(response({ error: 'unavailable' }, 503));
64
+ vi.stubGlobal('fetch', request);
65
+ expect((await provision()).status).toBe('expired');
66
+ await expect(provision()).rejects.toThrow('503');
67
+ });
68
+ it('requests a write token without a caller-selected deployment', async () => {
69
+ const credential = {
70
+ remote: ready.remote,
71
+ branch: 'main',
72
+ token: 'write-secret',
73
+ tokenExpiresAt: ready.expiresAt,
74
+ };
75
+ const request = vi.fn().mockResolvedValue(response(credential, 200));
76
+ vi.stubGlobal('fetch', request);
77
+ expect(await getAgentWriteToken('https://api.example.com', 'access')).toEqual(credential);
78
+ expect(request.mock.calls[0]?.[1].body).toBeUndefined();
79
+ });
80
+ });
@@ -0,0 +1,96 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createAgentClaimLink } from '../src/agent-claim.js';
4
+ import { DASHBOARD_URL } from '../src/constants.js';
5
+ import { deploymentInstructions, DEPLOY_HELP } from '../src/deploy-instructions.js';
6
+ import { renderWelcome } from '../src/welcome.js';
7
+
8
+ vi.mock('../src/agent-auth.js', () => ({
9
+ discoverAgentAuth: async () => ({
10
+ issuer: 'https://auth.example.com',
11
+ agent_auth: { claim_endpoint: 'https://auth.example.com/agent/identity/claim' },
12
+ }),
13
+ }));
14
+ afterEach(() => vi.unstubAllGlobals());
15
+ const credentials = {
16
+ registrationId: 'reg',
17
+ issuer: 'https://auth.example.com',
18
+ assertion: 'assertion-secret',
19
+ claimToken: 'claim-secret',
20
+ };
21
+ const site = {
22
+ status: 'ready' as const,
23
+ deploymentId: 'id',
24
+ subdomain: 'docs',
25
+ remote: 'https://git.example.com/repo',
26
+ branch: 'main' as const,
27
+ expiresAt: '2026-12-01T00:00:00.000Z',
28
+ };
29
+
30
+ describe('deploy help and claim handoff', () => {
31
+ it('uses the actual claim attempt API and returns its human-facing link', async () => {
32
+ const request = vi.fn().mockResolvedValue(
33
+ new Response(
34
+ JSON.stringify({
35
+ attempt: {
36
+ verification_uri:
37
+ 'https://auth.example.com/verify?token=attempt%2Btoken%26value&other=ignored',
38
+ },
39
+ })
40
+ )
41
+ );
42
+ vi.stubGlobal('fetch', request);
43
+ expect(
44
+ await createAgentClaimLink('https://api.example.com', credentials, 'user@example.com')
45
+ ).toBe(`${DASHBOARD_URL}/agent-claim?token=attempt%2Btoken%26value`);
46
+ expect(JSON.parse(request.mock.calls[0]?.[1].body)).toEqual({
47
+ type: 'service_auth',
48
+ claim_token: 'claim-secret',
49
+ login_hint: 'user@example.com',
50
+ });
51
+ });
52
+ it('rejects a verification link without an attempt token', async () => {
53
+ vi.stubGlobal(
54
+ 'fetch',
55
+ vi.fn().mockResolvedValue(
56
+ new Response(
57
+ JSON.stringify({
58
+ attempt: { verification_uri: 'https://auth.example.com/verify' },
59
+ })
60
+ )
61
+ )
62
+ );
63
+ await expect(
64
+ createAgentClaimLink('https://api.example.com', credentials, 'user@example.com')
65
+ ).rejects.toThrow();
66
+ });
67
+ it('keeps Git operations with the agent and omits identity credentials', () => {
68
+ const text = deploymentInstructions(
69
+ {
70
+ status: 'ready',
71
+ created: true,
72
+ site,
73
+ credentials,
74
+ write: {
75
+ remote: site.remote,
76
+ branch: site.branch,
77
+ token: 'write-secret',
78
+ tokenExpiresAt: site.expiresAt,
79
+ },
80
+ },
81
+ 'https://app.example.com/claim'
82
+ );
83
+ expect(text).toContain('Do not force-push');
84
+ expect(text).toContain('docs.json');
85
+ expect(text).toContain('https://app.example.com/claim');
86
+ expect(text).not.toContain('assertion-secret');
87
+ expect(text).not.toContain('claim-secret');
88
+ expect(text).not.toContain('write-secret');
89
+ });
90
+ it('documents the command in root help and explains that it does not push files', () => {
91
+ expect(renderWelcome({ packageName: 'mint', signedIn: false })).toContain('deploy');
92
+ expect(DEPLOY_HELP).toContain('Commit and push the documentation yourself');
93
+ expect(DEPLOY_HELP).toContain('--new');
94
+ expect(DEPLOY_HELP).toContain('ask the user for their email');
95
+ });
96
+ });
@@ -0,0 +1,155 @@
1
+ import path from 'node:path';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import * as auth from '../src/agent-auth.js';
5
+ import * as storage from '../src/agent-credentials.js';
6
+ import * as deployment from '../src/agent-deployment.js';
7
+ import * as config from '../src/config.js';
8
+ import { prepareAgentDeployment, formatDeployment } from '../src/deploy.js';
9
+
10
+ vi.mock('../src/agent-auth.js');
11
+ vi.mock('../src/agent-credentials.js');
12
+ vi.mock('../src/agent-deployment.js');
13
+ vi.mock('../src/config.js');
14
+
15
+ const credentials = {
16
+ registrationId: 'reg_1',
17
+ issuer: 'https://auth.example.com',
18
+ assertion: 'assertion',
19
+ claimToken: 'claim',
20
+ };
21
+ const site = {
22
+ status: 'ready' as const,
23
+ created: true,
24
+ deploymentId: 'id',
25
+ subdomain: 'allocated',
26
+ remote: 'https://git.example.com/repo',
27
+ branch: 'main' as const,
28
+ expiresAt: '2026-12-01T00:00:00.000Z',
29
+ };
30
+ const write = {
31
+ remote: site.remote,
32
+ branch: site.branch,
33
+ token: 'write',
34
+ tokenExpiresAt: site.expiresAt,
35
+ };
36
+ const options = {
37
+ apiUrl: 'https://api.example.com',
38
+ directory: path.resolve('docs'),
39
+ subdomain: 'requested',
40
+ };
41
+ const warn = vi.fn();
42
+
43
+ beforeEach(() => {
44
+ vi.resetAllMocks();
45
+ vi.mocked(storage.loadAgentCredentials).mockResolvedValue(null);
46
+ vi.mocked(storage.saveAgentCredentials).mockResolvedValue(undefined);
47
+ vi.mocked(auth.registerAgent).mockResolvedValue(credentials);
48
+ vi.mocked(auth.exchangeAgentToken).mockImplementation(async (_, value) => ({
49
+ ...value,
50
+ accessToken: 'access',
51
+ }));
52
+ vi.mocked(auth.getAgentIdentity).mockResolvedValue({
53
+ registrationId: 'reg_1',
54
+ identityType: 'anonymous',
55
+ });
56
+ vi.mocked(deployment.provisionAgentSite).mockResolvedValue(site);
57
+ vi.mocked(deployment.getAgentWriteToken).mockResolvedValue(write);
58
+ vi.mocked(config.setAgentSiteMetadata).mockResolvedValue(undefined);
59
+ });
60
+
61
+ describe('mint deploy', () => {
62
+ it.each([
63
+ ['https://leaves.mintlify.com', 'https://allocated.mintlify.site/'],
64
+ ['https://leaves-dev.mintlify.com', 'https://allocated.mint-dev.mintlify.me/'],
65
+ ])('prints the site URL for %s', async (apiUrl, siteUrl) => {
66
+ const result = await prepareAgentDeployment({ ...options, apiUrl }, warn);
67
+ expect(formatDeployment(result, apiUrl)).toContain(`Site: ${siteUrl}`);
68
+ });
69
+ it('continues provisioning after keyring save failure', async () => {
70
+ vi.mocked(storage.saveAgentCredentials).mockRejectedValue(new Error('locked'));
71
+ const result = await prepareAgentDeployment(options, warn);
72
+ expect(result.status).toBe('ready');
73
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('Continuing for this run'));
74
+ expect(formatDeployment(result, options.apiUrl)).toContain('https://allocated.mintlify.site');
75
+ });
76
+ it('persists the exchanged access token for later commands', async () => {
77
+ await prepareAgentDeployment(options, warn);
78
+ expect(storage.saveAgentCredentials).toHaveBeenLastCalledWith(
79
+ options.apiUrl,
80
+ options.directory,
81
+ { ...credentials, accessToken: 'access' }
82
+ );
83
+ });
84
+ it('reuses the current registration and the saved site name', async () => {
85
+ vi.mocked(storage.loadAgentCredentials).mockResolvedValue(credentials);
86
+ vi.mocked(config.getAgentSiteMetadata).mockReturnValue({
87
+ registrationId: 'reg_1',
88
+ subdomain: 'allocated',
89
+ });
90
+ await prepareAgentDeployment({ ...options, subdomain: undefined }, warn);
91
+ expect(auth.registerAgent).not.toHaveBeenCalled();
92
+ expect(deployment.provisionAgentSite).toHaveBeenCalledWith(options.apiUrl, 'access', {
93
+ subdomain: 'allocated',
94
+ orgName: undefined,
95
+ });
96
+ });
97
+ it('starts a new registration only when explicitly requested or the site expired', async () => {
98
+ vi.mocked(storage.loadAgentCredentials).mockResolvedValue(credentials);
99
+ await prepareAgentDeployment({ ...options, newSite: true }, warn);
100
+ expect(storage.loadAgentCredentials).not.toHaveBeenCalled();
101
+ expect(auth.registerAgent).toHaveBeenCalledTimes(1);
102
+ });
103
+ it('requires an explicit site name for --new even when a previous name is saved', async () => {
104
+ vi.mocked(config.getAgentSiteMetadata).mockReturnValue({
105
+ registrationId: 'reg_1',
106
+ subdomain: 'allocated',
107
+ });
108
+ await expect(
109
+ prepareAgentDeployment({ ...options, newSite: true, subdomain: undefined }, warn)
110
+ ).rejects.toThrow('Choose a site name with --subdomain');
111
+ expect(auth.discoverAgentAuth).not.toHaveBeenCalled();
112
+ expect(auth.registerAgent).not.toHaveBeenCalled();
113
+ expect(storage.saveAgentCredentials).not.toHaveBeenCalled();
114
+ expect(deployment.provisionAgentSite).not.toHaveBeenCalled();
115
+ });
116
+ it('starts again on confirmed deployment expiry', async () => {
117
+ vi.mocked(storage.loadAgentCredentials).mockResolvedValue(credentials);
118
+ vi.mocked(deployment.provisionAgentSite).mockResolvedValueOnce({
119
+ status: 'expired',
120
+ created: false,
121
+ code: 'deployment_already_exists',
122
+ deploymentId: 'old',
123
+ subdomain: 'old',
124
+ });
125
+ await prepareAgentDeployment(options, warn);
126
+ expect(auth.registerAgent).toHaveBeenCalledTimes(1);
127
+ expect(deployment.provisionAgentSite).toHaveBeenCalledTimes(2);
128
+ });
129
+ it('does not provision for a claimed identity', async () => {
130
+ vi.mocked(auth.getAgentIdentity).mockResolvedValue({
131
+ registrationId: 'reg_1',
132
+ identityType: 'user_bound',
133
+ userId: 'user',
134
+ orgId: 'org',
135
+ });
136
+ const result = await prepareAgentDeployment(options, warn);
137
+ expect(formatDeployment(result, options.apiUrl)).toContain('--new');
138
+ expect(deployment.provisionAgentSite).not.toHaveBeenCalled();
139
+ });
140
+ it('does not replace an existing identity after an authentication outage', async () => {
141
+ vi.mocked(storage.loadAgentCredentials).mockResolvedValue(credentials);
142
+ vi.mocked(auth.getAgentIdentity).mockRejectedValue(new Error('unavailable'));
143
+ await expect(prepareAgentDeployment(options, warn)).rejects.toThrow('unavailable');
144
+ expect(auth.registerAgent).not.toHaveBeenCalled();
145
+ });
146
+ it('does not create a site after failing to read known credentials', async () => {
147
+ vi.mocked(config.getAgentSiteMetadata).mockReturnValue({
148
+ registrationId: 'reg_1',
149
+ subdomain: 'allocated',
150
+ });
151
+ vi.mocked(storage.loadAgentCredentials).mockRejectedValue(new Error('locked'));
152
+ await expect(prepareAgentDeployment(options, warn)).rejects.toThrow('Could not read');
153
+ expect(auth.registerAgent).not.toHaveBeenCalled();
154
+ });
155
+ });
@@ -0,0 +1,103 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { z } from 'zod';
11
+ import { agentRequest, agentResponse } from './agent-http.js';
12
+ const resourceSchema = z.object({ authorization_servers: z.array(z.url()).min(1) });
13
+ const discoverySchema = z.object({
14
+ issuer: z.url(),
15
+ token_endpoint: z.url(),
16
+ agent_auth: z.object({
17
+ identity_endpoint: z.url(),
18
+ claim_endpoint: z.url(),
19
+ identity_types_supported: z.array(z.string()),
20
+ }),
21
+ });
22
+ const registrationSchema = z.object({
23
+ id: z.string().min(1),
24
+ identity: z.object({ assertion: z.string().min(1) }),
25
+ claim: z.object({ token: z.string().min(1) }),
26
+ });
27
+ const tokenSchema = z.object({
28
+ access_token: z.string().min(1),
29
+ expires_in: z.number().positive(),
30
+ token_type: z.string().regex(/^bearer$/i),
31
+ });
32
+ const identitySchema = z.discriminatedUnion('identityType', [
33
+ z.object({ registrationId: z.string(), identityType: z.literal('anonymous') }),
34
+ z.object({
35
+ registrationId: z.string(),
36
+ identityType: z.literal('user_bound'),
37
+ userId: z.string(),
38
+ orgId: z.string(),
39
+ }),
40
+ ]);
41
+ export function discoverAgentAuth(apiUrl) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ const resource = yield agentResponse(yield agentRequest(`${apiUrl}/api/agent/.well-known/oauth-protected-resource`), resourceSchema);
44
+ const issuer = resource.authorization_servers[0];
45
+ if (!issuer || new URL(issuer).protocol !== 'https:')
46
+ throw new Error('Agent authentication requires an HTTPS issuer.');
47
+ const discovery = yield agentResponse(yield agentRequest(`${issuer.replace(/\/$/, '')}/.well-known/oauth-authorization-server`), discoverySchema);
48
+ const origin = new URL(issuer).origin;
49
+ if ([
50
+ discovery.issuer,
51
+ discovery.token_endpoint,
52
+ discovery.agent_auth.identity_endpoint,
53
+ discovery.agent_auth.claim_endpoint,
54
+ ].some((url) => new URL(url).origin !== origin)) {
55
+ throw new Error('Agent authentication endpoints do not match the configured issuer.');
56
+ }
57
+ return discovery;
58
+ });
59
+ }
60
+ export function registerAgent(discovery) {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ if (!discovery.agent_auth.identity_types_supported.includes('anonymous')) {
63
+ throw new Error('Anonymous registration is not enabled for this environment.');
64
+ }
65
+ const result = yield agentResponse(yield agentRequest(discovery.agent_auth.identity_endpoint, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ type: 'anonymous' }),
69
+ }), registrationSchema);
70
+ return {
71
+ registrationId: result.id,
72
+ issuer: discovery.issuer,
73
+ assertion: result.identity.assertion,
74
+ claimToken: result.claim.token,
75
+ };
76
+ });
77
+ }
78
+ export function exchangeAgentToken(discovery, credentials) {
79
+ return __awaiter(this, void 0, void 0, function* () {
80
+ var _a;
81
+ if (new URL(credentials.issuer).origin !== new URL(discovery.issuer).origin) {
82
+ throw new Error('Saved agent credentials belong to another issuer.');
83
+ }
84
+ if (credentials.accessToken && ((_a = credentials.accessTokenExpiresAt) !== null && _a !== void 0 ? _a : 0) > Date.now() + 30000) {
85
+ return Object.assign(Object.assign({}, credentials), { accessToken: credentials.accessToken });
86
+ }
87
+ const token = yield agentResponse(yield agentRequest(discovery.token_endpoint, {
88
+ method: 'POST',
89
+ body: new URLSearchParams({
90
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
91
+ assertion: credentials.assertion,
92
+ }),
93
+ }), tokenSchema);
94
+ return Object.assign(Object.assign({}, credentials), { accessToken: token.access_token, accessTokenExpiresAt: Date.now() + token.expires_in * 1000 });
95
+ });
96
+ }
97
+ export function getAgentIdentity(apiUrl, accessToken) {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ return agentResponse(yield agentRequest(`${apiUrl}/api/agent/identity`, {
100
+ headers: { Authorization: `Bearer ${accessToken}` },
101
+ }), identitySchema);
102
+ });
103
+ }
@@ -0,0 +1,39 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { z } from 'zod';
11
+ import { discoverAgentAuth } from './agent-auth.js';
12
+ import { agentRequest, agentResponse } from './agent-http.js';
13
+ import { DASHBOARD_URL } from './constants.js';
14
+ const claimAttemptSchema = z.object({ attempt: z.object({ verification_uri: z.url() }) });
15
+ export function createAgentClaimLink(apiUrl, credentials, email) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ const loginHint = z.email().parse(email);
18
+ const discovery = yield discoverAgentAuth(apiUrl);
19
+ if (new URL(discovery.issuer).origin !== new URL(credentials.issuer).origin) {
20
+ throw new Error('Saved agent credentials belong to another issuer.');
21
+ }
22
+ const claim = yield agentResponse(yield agentRequest(discovery.agent_auth.claim_endpoint, {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({
26
+ type: 'service_auth',
27
+ claim_token: credentials.claimToken,
28
+ login_hint: loginHint,
29
+ }),
30
+ }), claimAttemptSchema);
31
+ const token = z
32
+ .string()
33
+ .min(1)
34
+ .parse(new URL(claim.attempt.verification_uri).searchParams.get('token'));
35
+ const claimUrl = new URL('/agent-claim', DASHBOARD_URL);
36
+ claimUrl.searchParams.set('token', token);
37
+ return claimUrl.toString();
38
+ });
39
+ }