Skip to content

Commit 22bf81f

Browse files
Merge pull request #1484 from salesforcecli/sl/W-19126474
Update org open for single-use- W-19126474
2 parents a88991e + a69571f commit 22bf81f

File tree

8 files changed

+72
-162
lines changed

8 files changed

+72
-162
lines changed

messages/messages.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ This command will expose sensitive information that allows for subsequent activi
44
Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege.
55
For additional information, please review the authorization section of the https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_web_flow.htm.
66

7-
# BehaviorChangeWarning
8-
9-
Starting in August 2025, this command will generate single-use URLs when you specify either the --json or --url-only (-r) flag. These URLs can be used only one time; subsequent use won't allow you to log in to the org.
10-
117
# SingleAccessFrontdoorError
128

13-
Failed to generate a single-use frontdoor URL.
9+
Failed to generate a frontdoor URL.

src/commands/org/open.ts

Lines changed: 15 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import path from 'node:path';
98
import {
109
Flags,
1110
loglevel,
1211
orgApiVersionFlagWithDeprecations,
1312
requiredOrgFlagWithDeprecations,
1413
} from '@salesforce/sf-plugins-core';
15-
import { Connection, Messages } from '@salesforce/core';
14+
import { Messages, Org } from '@salesforce/core';
1615
import { MetadataResolver } from '@salesforce/source-deploy-retrieve';
17-
import { env } from '@salesforce/kit';
1816
import { buildFrontdoorUrl } from '../../shared/orgOpenUtils.js';
1917
import { OrgOpenCommandBase } from '../../shared/orgOpenCommandBase.js';
2018
import { type OrgOpenOutput } from '../../shared/orgTypes.js';
2119

2220
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2321
const messages = Messages.loadMessages('@salesforce/plugin-org', 'open');
24-
const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages');
2522

2623
export class OrgOpenCommand extends OrgOpenCommandBase<OrgOpenOutput> {
2724
public static readonly summary = messages.getMessage('summary');
@@ -68,75 +65,39 @@ export class OrgOpenCommand extends OrgOpenCommandBase<OrgOpenOutput> {
6865
};
6966

7067
public async run(): Promise<OrgOpenOutput> {
71-
this.warn(sharedMessages.getMessage('BehaviorChangeWarning'));
7268
const { flags } = await this.parse(OrgOpenCommand);
7369
this.org = flags['target-org'];
7470
this.connection = this.org.getConnection(flags['api-version']);
7571

76-
const singleUseEnvVar: boolean = env.getBoolean('SF_SINGLE_USE_ORG_OPEN_URL');
77-
const singleUseMode = singleUseEnvVar ? singleUseEnvVar : !(flags['url-only'] || this.jsonEnabled());
7872
const [frontDoorUrl, retUrl] = await Promise.all([
79-
buildFrontdoorUrl(this.org, this.connection, singleUseMode),
80-
flags['source-file'] ? generateFileUrl(flags['source-file'], this.connection) : flags.path,
73+
buildFrontdoorUrl(this.org),
74+
flags['source-file'] ? generateFileUrl(flags['source-file'], this.org) : flags.path,
8175
]);
8276

8377
return this.openOrgUI(flags, frontDoorUrl, retUrl);
8478
}
8579
}
8680

87-
const generateFileUrl = async (file: string, conn: Connection): Promise<string> => {
81+
const generateFileUrl = async (file: string, org: Org): Promise<string> => {
8882
try {
8983
const metadataResolver = new MetadataResolver();
9084
const components = metadataResolver.getComponentsFromPath(file);
9185
const typeName = components[0]?.type?.name;
9286

93-
switch (typeName) {
94-
case 'Bot':
95-
return `AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${await botFileNameToId(conn, file)}`;
96-
case 'ApexPage':
97-
return `/apex/${path.basename(file).replace('.page-meta.xml', '').replace('.page', '')}`;
98-
case 'Flow':
99-
return `/builder_platform_interaction/flowBuilder.app?flowId=${await flowFileNameToId(conn, file)}`;
100-
case 'FlexiPage':
101-
return `/visualEditor/appBuilder.app?pageId=${await flexiPageFilenameToId(conn, file)}`;
102-
default:
103-
return 'lightning/setup/FlexiPageList/home';
87+
if (!typeName) {
88+
throw new Error(`Unable to determine metadata type for file: ${file}`);
10489
}
90+
91+
return await org.getMetadataUIURL(typeName, file);
10592
} catch (error) {
106-
if (error instanceof Error && error.name === 'FlowIdNotFoundError') {
93+
if (
94+
error instanceof Error &&
95+
(error.message.includes('FlowIdNotFound') ||
96+
error.message.includes('CustomObjectIdNotFound') ||
97+
error.message.includes('ApexClassIdNotFound'))
98+
) {
10799
throw error;
108100
}
109-
return 'lightning/setup/FlexiPageList/home';
110-
}
111-
};
112-
113-
const botFileNameToId = async (conn: Connection, filePath: string): Promise<string> =>
114-
(
115-
await conn.singleRecordQuery<{ Id: string }>(
116-
`SELECT id FROM BotDefinition WHERE DeveloperName='${path.basename(filePath, '.bot-meta.xml')}'`
117-
)
118-
).Id;
119-
120-
/** query flexipage via toolingPAI to get its ID (starts with 0M0) */
121-
const flexiPageFilenameToId = async (conn: Connection, filePath: string): Promise<string> =>
122-
(
123-
await conn.singleRecordQuery<{ Id: string }>(
124-
`SELECT id FROM flexipage WHERE DeveloperName='${path.basename(filePath, '.flexipage-meta.xml')}'`,
125-
{ tooling: true }
126-
)
127-
).Id;
128-
129-
/** query the rest API to turn a flow's filepath into a FlowId (starts with 301) */
130-
const flowFileNameToId = async (conn: Connection, filePath: string): Promise<string> => {
131-
try {
132-
const flow = await conn.singleRecordQuery<{ DurableId: string }>(
133-
`SELECT DurableId FROM FlowVersionView WHERE FlowDefinitionView.ApiName = '${path.basename(
134-
filePath,
135-
'.flow-meta.xml'
136-
)}' ORDER BY VersionNumber DESC LIMIT 1`
137-
);
138-
return flow.DurableId;
139-
} catch (error) {
140-
throw messages.createError('FlowIdNotFound', [filePath]);
101+
return '';
141102
}
142103
};

src/commands/org/open/agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class OrgOpenAgent extends OrgOpenCommandBase<OrgOpenOutput> {
5252
this.connection = this.org.getConnection(flags['api-version']);
5353

5454
const [frontDoorUrl, retUrl] = await Promise.all([
55-
buildFrontdoorUrl(this.org, this.connection, true),
55+
buildFrontdoorUrl(this.org),
5656
buildRetUrl(this.connection, flags['api-name']),
5757
]);
5858

src/shared/orgOpenCommandBase.ts

Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import path from 'node:path';
9-
import fs from 'node:fs';
10-
import { platform, tmpdir } from 'node:os';
11-
import { execSync } from 'node:child_process';
12-
import isWsl from 'is-wsl';
8+
import { platform } from 'node:os';
139
import { apps } from 'open';
1410
import { SfCommand } from '@salesforce/sf-plugins-core';
1511
import { Connection, Messages, Org, SfdcUrl, SfError } from '@salesforce/core';
16-
import { env, sleep } from '@salesforce/kit';
17-
import utils, { fileCleanup, getFileContents, handleDomainError } from './orgOpenUtils.js';
12+
import { env } from '@salesforce/kit';
13+
import utils, { handleDomainError } from './orgOpenUtils.js';
1814
import { type OrgOpenOutput } from './orgTypes.js';
1915

2016
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
@@ -46,8 +42,8 @@ export abstract class OrgOpenCommandBase<T> extends SfCommand<T> {
4642
// NOTE: Deliberate use of `||` here since getBoolean() defaults to false, and we need to consider both env vars.
4743
const containerMode = env.getBoolean('SF_CONTAINER_MODE') || env.getBoolean('SFDX_CONTAINER_MODE');
4844

49-
// security warning only for --json OR --url-only OR containerMode
50-
if (flags['url-only'] || this.jsonEnabled() || containerMode) {
45+
// security warning only for --url-only OR containerMode
46+
if (flags['url-only'] || containerMode) {
5147
const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages');
5248
this.warn(sharedMessages.getMessage('SecurityWarning'));
5349
this.log('');
@@ -76,46 +72,13 @@ export abstract class OrgOpenCommandBase<T> extends SfCommand<T> {
7672
handleDomainError(err, url, env);
7773
}
7874

79-
if (this.jsonEnabled()) {
80-
// TODO: remove this code path once the org open behavior changes on August 2025 (see W-17661469)
81-
// create a local html file that contains the POST stuff.
82-
const tempFilePath = path.join(tmpdir(), `org-open-${new Date().valueOf()}.html`);
83-
await fs.promises.writeFile(
84-
tempFilePath,
85-
getFileContents(
86-
this.connection.accessToken as string,
87-
this.connection.instanceUrl,
88-
// the path flag is URI-encoded in its `parse` func.
89-
// For the form redirect to work we need it decoded.
90-
flags.path ? decodeURIComponent(flags.path) : retUrl
91-
)
92-
);
93-
const filePathUrl = isWsl
94-
? 'file:///' + execSync(`wslpath -m ${tempFilePath}`).toString().trim()
95-
: `file:///${tempFilePath}`;
96-
const cp = await utils.openUrl(filePathUrl, {
97-
...(flags.browser ? { app: { name: apps[flags.browser] } } : {}),
98-
...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}),
99-
});
100-
cp.on('error', (err) => {
101-
fileCleanup(tempFilePath);
102-
throw SfError.wrap(err);
103-
});
104-
// so we don't delete the file while the browser is still using it
105-
// open returns when the CP is spawned, but there's not way to know if the browser is still using the file
106-
await sleep(platform() === 'win32' || isWsl ? 7000 : 5000);
107-
fileCleanup(tempFilePath);
108-
} else {
109-
// it means we generated a one-time use frontdoor url
110-
// so the workaround to create a local html file is not needed
111-
const cp = await utils.openUrl(url, {
112-
...(flags.browser ? { app: { name: apps[flags.browser] } } : {}),
113-
...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}),
114-
});
115-
cp.on('error', (err) => {
116-
throw SfError.wrap(err);
117-
});
118-
}
75+
const cp = await utils.openUrl(url, {
76+
...(flags.browser ? { app: { name: apps[flags.browser] } } : {}),
77+
...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}),
78+
});
79+
cp.on('error', (err) => {
80+
throw SfError.wrap(err);
81+
});
11982

12083
return output;
12184
}

src/shared/orgOpenUtils.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { rmSync } from 'node:fs';
99
import { ChildProcess } from 'node:child_process';
1010
import open, { Options } from 'open';
11-
import { Connection, Logger, Messages, Org, SfError } from '@salesforce/core';
11+
import { Logger, Messages, Org, SfError } from '@salesforce/core';
1212
import { Duration, Env } from '@salesforce/kit';
1313

1414
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
@@ -20,35 +20,19 @@ export const openUrl = async (url: string, options: Options): Promise<ChildProce
2020
export const fileCleanup = (tempFilePath: string): void =>
2121
rmSync(tempFilePath, { force: true, maxRetries: 3, recursive: true });
2222

23-
type SingleAccessUrlRes = { frontdoor_uri: string | undefined };
24-
2523
/**
2624
* This method generates and returns a frontdoor url for the given org.
2725
*
2826
* @param org org for which we generate the frontdoor url.
29-
* @param conn the Connection for the given Org.
30-
* @param singleUseUrl if true returns a single-use url frontdoor url.
3127
*/
32-
export const buildFrontdoorUrl = async (org: Org, conn: Connection, singleUseUrl: boolean): Promise<string> => {
33-
await org.refreshAuth(); // we need a live accessToken for the frontdoor url
34-
const accessToken = conn.accessToken;
35-
if (!accessToken) {
36-
throw new SfError('NoAccessToken', 'NoAccessToken');
37-
}
38-
if (singleUseUrl) {
39-
try {
40-
const response: SingleAccessUrlRes = await conn.requestGet('/services/oauth2/singleaccess');
41-
if (response.frontdoor_uri) return response.frontdoor_uri;
42-
throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError')).setData(response);
43-
} catch (e) {
44-
if (e instanceof SfError) throw e;
45-
const err = e as Error;
46-
throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError'), err.message);
47-
}
48-
} else {
49-
// TODO: remove this code path once the org open behavior changes on August 2025 (see W-17661469)
50-
const instanceUrlClean = org.getField<string>(Org.Fields.INSTANCE_URL).replace(/\/$/, '');
51-
return `${instanceUrlClean}/secur/frontdoor.jsp?sid=${accessToken}`;
28+
29+
export const buildFrontdoorUrl = async (org: Org): Promise<string> => {
30+
try {
31+
return await org.getFrontDoorUrl();
32+
} catch (e) {
33+
if (e instanceof SfError) throw e;
34+
const err = e as Error;
35+
throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError'), err.message);
5236
}
5337
};
5438

test/nut/listAndDisplay.nut.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe('Org Command NUT', () => {
213213
expect(result).to.include({ orgId: aliasUserOrgId, username: aliasedUsername });
214214
expect(result)
215215
.to.property('url')
216-
.to.include(`retURL=${encodeURIComponent(decodeURIComponent('foo/bar/baz'))}`);
216+
.to.include(`startURL=${encodeURIComponent(decodeURIComponent('foo/bar/baz'))}`);
217217
});
218218
});
219219
});

test/nut/open.nut.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ describe('test org:open command', () => {
4343
defaultUserOrgId = defaultOrg.orgId as string;
4444
});
4545

46-
it('should produce the default URL for a flexipage resource when it not in org in json', () => {
46+
it('should produce the frontdoor default URL for a flexipage resource when it not in org in json', () => {
4747
const result = execCmd<OrgOpenOutput>(`force:source:open -f ${flexiPagePath} --urlonly --json`, {
4848
ensureExitCode: 0,
4949
}).jsonOutput?.result;
5050
assert(result);
5151
expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername });
52-
expect(result.url).to.include('lightning/setup/FlexiPageList/home');
52+
expect(result.url).to.include('secur/frontdoor.jsp');
5353
});
5454

5555
it('should produce the URL for a flexipage resource in json', async () => {
@@ -63,7 +63,7 @@ describe('test org:open command', () => {
6363
}).jsonOutput?.result;
6464
assert(result);
6565
expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername });
66-
expect(result.url).to.include('/visualEditor/appBuilder.app?pageId');
66+
expect(result.url).to.include('secur/frontdoor.jsp');
6767
});
6868

6969
it('should produce the URL for an existing flow', () => {
@@ -72,7 +72,7 @@ describe('test org:open command', () => {
7272
}).jsonOutput?.result;
7373
assert(result);
7474
expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername });
75-
expect(result.url).to.include('/builder_platform_interaction/flowBuilder.app?flowId=301');
75+
expect(result.url).to.include('secur/frontdoor.jsp');
7676
});
7777

7878
it("should produce the org's frontdoor url when edition of file is not supported", async () => {

0 commit comments

Comments
 (0)