@factorialco/fcode-cli 2.2.0 ā 2.3.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/README.md +1 -1
- package/dist/commands/clone.js +14 -1
- package/dist/core/commands/api-client-command.d.ts +1 -0
- package/dist/core/commands/api-client-command.js +5 -0
- package/dist/core/commands/base-command.d.ts +2 -0
- package/dist/core/commands/base-command.js +31 -0
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.js +16 -1
- package/dist/core/types.js +3 -1
- package/dist/services/auth-service.d.ts +1 -0
- package/dist/services/auth-service.js +12 -0
- package/dist/services/fcode-graphql-client.d.ts +2 -0
- package/dist/services/fcode-graphql-client.js +26 -1
- package/dist/services/locale-merger.d.ts +1 -1
- package/dist/services/locale-merger.js +16 -13
- package/dist/services/team-access.d.ts +13 -0
- package/dist/services/team-access.js +45 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ $ npm install -g @factorialco/fcode-cli
|
|
|
20
20
|
$ fcode COMMAND
|
|
21
21
|
running command...
|
|
22
22
|
$ fcode (--version)
|
|
23
|
-
@factorialco/fcode-cli/2.
|
|
23
|
+
@factorialco/fcode-cli/2.3.0 linux-x64 node-v22.22.3
|
|
24
24
|
$ fcode --help [COMMAND]
|
|
25
25
|
USAGE
|
|
26
26
|
$ fcode COMMAND
|
package/dist/commands/clone.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require("node:path");
|
|
|
7
7
|
const global_1 = require("../config/global");
|
|
8
8
|
const workspace_1 = require("../config/workspace");
|
|
9
9
|
const pull_1 = require("./pull");
|
|
10
|
+
const team_access_1 = require("../services/team-access");
|
|
10
11
|
const types_1 = require("../core/types");
|
|
11
12
|
const cli_1 = require("../utils/cli");
|
|
12
13
|
const settings_1 = require("../config/settings");
|
|
@@ -120,6 +121,7 @@ class Clone extends core_1.Command {
|
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
async catch(error) {
|
|
124
|
+
process.exitCode = 1;
|
|
123
125
|
const suggestions = error?.suggestions ?? [];
|
|
124
126
|
const hint = suggestions.length > 0 ? ` Try this: ${suggestions.map((suggestion) => `'${suggestion}'`).join('\n')} \n` : '';
|
|
125
127
|
cli_1.default.error(error.message + hint);
|
|
@@ -180,8 +182,19 @@ class Clone extends core_1.Command {
|
|
|
180
182
|
if (skipSkillsSetup === undefined) {
|
|
181
183
|
skipSkillsSetup = (await new global_1.default().getSetting(settings_1.SettingKeys.SKIP_SKILLS_SETUP)) === 'true';
|
|
182
184
|
}
|
|
185
|
+
// Pre-flight access check so a session without access to the team fails here,
|
|
186
|
+
// before any workspace directory, config, or skills are created.
|
|
187
|
+
await team_access_1.default.ensureTeamAccess(url, slug);
|
|
183
188
|
await this.cloneTeam({ url, slug }, skipSkillsSetup);
|
|
184
|
-
|
|
189
|
+
// Failed pulls don't abort the clone: each sub-pull reports through
|
|
190
|
+
// BaseCommand.catch, which prints and sets a non-zero exit code.
|
|
191
|
+
if (process.exitCode) {
|
|
192
|
+
cli_1.default.log(`\nā ļø '${args.team}' team workspace cloned with errors ā some resources failed to pull.`);
|
|
193
|
+
cli_1.default.log(` Fix the errors above, then run 'fcode pull' inside the workspace.\n`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
cli_1.default.log(`\nš ${args.team} team workspace cloned successfully!\n`);
|
|
197
|
+
}
|
|
185
198
|
cli_1.default.log(`$ cd ${slug}\n`);
|
|
186
199
|
}
|
|
187
200
|
}
|
|
@@ -26,5 +26,10 @@ class ApiClientCommand extends base_command_1.default {
|
|
|
26
26
|
throw error;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
async onSessionRefreshed() {
|
|
30
|
+
const { url } = await this.workspaceConfig.getTeamOrigin();
|
|
31
|
+
const { token: { access_token: accessToken }, } = await this.globalConfig.getAuthInfo(url);
|
|
32
|
+
this._fcodeGraphQLClient.setAccessToken(accessToken);
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
exports.default = ApiClientCommand;
|
|
@@ -5,7 +5,9 @@ export default abstract class BaseCommand extends Command {
|
|
|
5
5
|
url?: string;
|
|
6
6
|
globalConfig: GlobalConfig;
|
|
7
7
|
workspaceConfig: WorkspaceConfig;
|
|
8
|
+
private sessionRefreshRetried;
|
|
8
9
|
catch(error: any): Promise<any>;
|
|
9
10
|
init(url?: string): Promise<void>;
|
|
11
|
+
protected onSessionRefreshed(): Promise<void>;
|
|
10
12
|
protected logActiveTeam(): Promise<void>;
|
|
11
13
|
}
|
|
@@ -4,16 +4,42 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const global_1 = require("../../config/global");
|
|
5
5
|
const workspace_1 = require("../../config/workspace");
|
|
6
6
|
const color_1 = require("@oclif/color");
|
|
7
|
+
const auth_service_1 = require("../../services/auth-service");
|
|
7
8
|
const errors_1 = require("../errors");
|
|
8
9
|
const cli_1 = require("../../utils/cli");
|
|
9
10
|
const url_utils_1 = require("../../utils/url-utils");
|
|
10
11
|
class BaseCommand extends core_1.Command {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.sessionRefreshRetried = false;
|
|
15
|
+
}
|
|
11
16
|
async catch(error) {
|
|
12
17
|
// do any extra work with error
|
|
13
18
|
if (error && error.response && error.response.status === 401) {
|
|
19
|
+
process.exitCode = 1;
|
|
14
20
|
cli_1.default.error(`User authentication is invalid for current team.`);
|
|
15
21
|
return;
|
|
16
22
|
}
|
|
23
|
+
if (error && error.response && error.response.status === 403) {
|
|
24
|
+
let origin;
|
|
25
|
+
try {
|
|
26
|
+
origin = await this.workspaceConfig.getTeamOrigin();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// No workspace context; report without the slug.
|
|
30
|
+
}
|
|
31
|
+
// A 403 with a valid session usually means the token's team groups went
|
|
32
|
+
// stale ā they are snapshotted at issuance (FCT-61231). Re-mint the token
|
|
33
|
+
// and retry the command once; a second denial falls through to the error.
|
|
34
|
+
if (!this.sessionRefreshRetried && origin && (await auth_service_1.default.refreshSession(origin.url))) {
|
|
35
|
+
this.sessionRefreshRetried = true;
|
|
36
|
+
await this.onSessionRefreshed();
|
|
37
|
+
return this.run().catch((retryError) => this.catch(retryError));
|
|
38
|
+
}
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
cli_1.default.error((0, errors_1.teamAccessDeniedMessage)(origin?.slug));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
17
43
|
if (error instanceof errors_1.NotAuthenticated) {
|
|
18
44
|
let errorMessage = 'No session active. Logging you in';
|
|
19
45
|
const loginArgs = [];
|
|
@@ -40,9 +66,11 @@ class BaseCommand extends core_1.Command {
|
|
|
40
66
|
return this.run();
|
|
41
67
|
}
|
|
42
68
|
if (error instanceof errors_1.CLIError) {
|
|
69
|
+
process.exitCode = 1;
|
|
43
70
|
cli_1.default.error(error);
|
|
44
71
|
return;
|
|
45
72
|
}
|
|
73
|
+
process.exitCode = 1;
|
|
46
74
|
cli_1.default.error(error.message);
|
|
47
75
|
}
|
|
48
76
|
async init(url) {
|
|
@@ -53,6 +81,9 @@ class BaseCommand extends core_1.Command {
|
|
|
53
81
|
const { authUrl } = await this.globalConfig.getAuthInfo(workspaceUrl);
|
|
54
82
|
await this.config.runHook('auth', { argv: [workspaceUrl, authUrl], id: 'init' });
|
|
55
83
|
}
|
|
84
|
+
// Called after a 403-triggered session refresh, before the retry: rewire
|
|
85
|
+
// anything that captured the old access token. API commands override this.
|
|
86
|
+
async onSessionRefreshed() { }
|
|
56
87
|
async logActiveTeam() {
|
|
57
88
|
const { slug, url } = await this.workspaceConfig.getTeamOrigin();
|
|
58
89
|
cli_1.default.log(color_1.default.magenta(`š» Working on Factorial Code team '${slug}${url !== global_1.FCODE_CLOUD_URL ? ` (${url})` : ''}'.`));
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export declare class NotAuthenticated extends CLIError {
|
|
|
47
47
|
constructor(url: string);
|
|
48
48
|
getUrl(): string;
|
|
49
49
|
}
|
|
50
|
+
export declare function teamAccessDeniedMessage(slug?: string): string;
|
|
51
|
+
export declare class TeamAccessDenied extends CLIError {
|
|
52
|
+
constructor(slug?: string);
|
|
53
|
+
}
|
|
50
54
|
export declare class NotFcodeWorkspace extends CLIError {
|
|
51
55
|
constructor();
|
|
52
56
|
}
|
package/dist/core/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ErrorExecutingProcess = exports.ProcessDependenciesNotInstalled = exports.DependenciesNotInstalled = exports.NotFcodeWorkspace = exports.NotAuthenticated = exports.CloudProgrammingLanguageManifestNotFound = exports.LocalProgrammingLanguageManifestNotFound = exports.CloudLocaleNotFound = exports.LocalLocaleNotFound = exports.CloudVariableNotFound = exports.LocalVariableNotFound = exports.CloudScriptLibraryNotFound = exports.LocalScriptLibraryNotFound = exports.CloudProcessNotFound = exports.LocalProcessNotFound = exports.ResourceNotFound = exports.CLIError = void 0;
|
|
3
|
+
exports.ErrorExecutingProcess = exports.ProcessDependenciesNotInstalled = exports.DependenciesNotInstalled = exports.NotFcodeWorkspace = exports.TeamAccessDenied = exports.NotAuthenticated = exports.CloudProgrammingLanguageManifestNotFound = exports.LocalProgrammingLanguageManifestNotFound = exports.CloudLocaleNotFound = exports.LocalLocaleNotFound = exports.CloudVariableNotFound = exports.LocalVariableNotFound = exports.CloudScriptLibraryNotFound = exports.LocalScriptLibraryNotFound = exports.CloudProcessNotFound = exports.LocalProcessNotFound = exports.ResourceNotFound = exports.CLIError = void 0;
|
|
4
|
+
exports.teamAccessDeniedMessage = teamAccessDeniedMessage;
|
|
4
5
|
const global_1 = require("../config/global");
|
|
5
6
|
class CLIError extends Error {
|
|
6
7
|
constructor(message, options) {
|
|
@@ -120,6 +121,20 @@ class NotAuthenticated extends CLIError {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
exports.NotAuthenticated = NotAuthenticated;
|
|
124
|
+
function teamAccessDeniedMessage(slug) {
|
|
125
|
+
const team = slug ? ` '${slug}'` : '';
|
|
126
|
+
return (`You don't have access to team${team} with your current session.` +
|
|
127
|
+
` If you were granted access recently, run 'fcode logout' and 'fcode login' and retry.`);
|
|
128
|
+
}
|
|
129
|
+
class TeamAccessDenied extends CLIError {
|
|
130
|
+
constructor(slug) {
|
|
131
|
+
super(teamAccessDeniedMessage(slug), {
|
|
132
|
+
code: 'ERR_TEAM_ACCESS_DENIED',
|
|
133
|
+
exit: 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.TeamAccessDenied = TeamAccessDenied;
|
|
123
138
|
class NotFcodeWorkspace extends CLIError {
|
|
124
139
|
constructor() {
|
|
125
140
|
super('ā ļø Current directory is not a Factorial Code workspace.', {
|
package/dist/core/types.js
CHANGED
|
@@ -214,7 +214,9 @@ class Team extends Resource {
|
|
|
214
214
|
const payload = {
|
|
215
215
|
zoneId: this.zoneId || null,
|
|
216
216
|
errorHandlerConfig: this.errorHandlerConfig || null,
|
|
217
|
-
|
|
217
|
+
// Kept in the given order, not sorted: the order is the inheritance precedence, so a pure
|
|
218
|
+
// reorder is a real change that must be detected and pushed.
|
|
219
|
+
parentTeamSlugs: [...(this.parentTeamSlugs || [])],
|
|
218
220
|
};
|
|
219
221
|
// Inserted only when set, so every team.json that predates webhook auth keeps the
|
|
220
222
|
// hash already stored in .fcode and does not report a phantom conflict. A key present
|
|
@@ -8,6 +8,7 @@ declare const AuthService: {
|
|
|
8
8
|
isAuthenticated(url: string): Promise<boolean>;
|
|
9
9
|
isTokenExpiring(token: Token): boolean;
|
|
10
10
|
refreshToken(authUrl: string, refreshToken: string): Promise<Token>;
|
|
11
|
+
refreshSession(url: string): Promise<boolean>;
|
|
11
12
|
tryRefresh(url: string, { authUrl, token, userInfo }: AuthInfo): Promise<boolean>;
|
|
12
13
|
login({ url, authUrl, email, password, clientId, clientSecret, accessToken }: Credentials): Promise<AuthInfo>;
|
|
13
14
|
logout(url: string): Promise<void>;
|
|
@@ -117,6 +117,18 @@ const AuthService = {
|
|
|
117
117
|
}
|
|
118
118
|
throw token.error_description;
|
|
119
119
|
},
|
|
120
|
+
// Force a re-mint of the access token regardless of expiry. Team groups are
|
|
121
|
+
// snapshotted into the token at issuance, so a token created before a team
|
|
122
|
+
// existed (or before the user joined it) 403s on it until re-minted (FCT-61231).
|
|
123
|
+
async refreshSession(url) {
|
|
124
|
+
try {
|
|
125
|
+
const authInfo = await this.globalConfig.getAuthInfo(url);
|
|
126
|
+
return await this.tryRefresh(url, authInfo);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
120
132
|
async tryRefresh(url, { authUrl, token, userInfo }) {
|
|
121
133
|
if (!token.refresh_token) {
|
|
122
134
|
return false;
|
|
@@ -10,6 +10,7 @@ declare class FcodeGraphQLClient {
|
|
|
10
10
|
private teamSlug;
|
|
11
11
|
private url;
|
|
12
12
|
constructor(options: FcodeGraphQLClientOptions);
|
|
13
|
+
setAccessToken(accessToken: string): void;
|
|
13
14
|
isDuplicatedError(error: any): boolean;
|
|
14
15
|
isEmptyValueError(error: any): boolean;
|
|
15
16
|
cleanError(error: any): any;
|
|
@@ -63,6 +64,7 @@ declare class FcodeGraphQLClient {
|
|
|
63
64
|
/** The API upserts: an existing alias with the same name is re-pointed to the tag. */
|
|
64
65
|
createTeamVersionAlias(name: string, tag: string): Promise<TeamVersionAlias>;
|
|
65
66
|
deleteTeamVersionAlias(id: string): Promise<TeamVersionAlias>;
|
|
67
|
+
verifyTeamAccess(): Promise<void>;
|
|
66
68
|
team(): Promise<RawTeamSettings>;
|
|
67
69
|
pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
|
|
68
70
|
mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
|
|
@@ -90,6 +90,11 @@ class FcodeGraphQLClient {
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
+
// Swap the bearer token on the live client (after a session refresh) so every
|
|
94
|
+
// holder of this instance ā repositories, pushers, queries ā uses the new one.
|
|
95
|
+
setAccessToken(accessToken) {
|
|
96
|
+
this.client.setHeader('authorization', `Bearer ${accessToken}`);
|
|
97
|
+
}
|
|
93
98
|
isDuplicatedError(error) {
|
|
94
99
|
return (error &&
|
|
95
100
|
error.response &&
|
|
@@ -345,7 +350,10 @@ class FcodeGraphQLClient {
|
|
|
345
350
|
catch (error) {
|
|
346
351
|
return Promise.reject(this.cleanError(error));
|
|
347
352
|
}
|
|
348
|
-
|
|
353
|
+
// The cloud team's parent order is the inheritance precedence the merge must replicate; the
|
|
354
|
+
// local team.json may lag behind it, so the cloud is asked, not the file.
|
|
355
|
+
const { parentTeamSlugs } = await this.team();
|
|
356
|
+
const merged = (0, locale_merger_1.mergeInheritedLocales)(locales, parentTeamSlugs ?? []);
|
|
349
357
|
(0, locale_merger_1.assertLocaleFilesCanCoexist)(merged);
|
|
350
358
|
return merged;
|
|
351
359
|
}
|
|
@@ -860,6 +868,23 @@ class FcodeGraphQLClient {
|
|
|
860
868
|
return Promise.reject(this.cleanError(error));
|
|
861
869
|
});
|
|
862
870
|
}
|
|
871
|
+
// Cheapest team-scoped query, used as a pre-flight access check so commands can
|
|
872
|
+
// fail once with a clear error before touching local state, instead of erroring
|
|
873
|
+
// on every resource pull.
|
|
874
|
+
verifyTeamAccess() {
|
|
875
|
+
return this.client
|
|
876
|
+
.request((0, graphql_request_1.gql) `
|
|
877
|
+
query team($slug: ID!) {
|
|
878
|
+
team(slug: $slug) {
|
|
879
|
+
slug
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
`, { slug: this.teamSlug })
|
|
883
|
+
.then(() => undefined)
|
|
884
|
+
.catch((error) => {
|
|
885
|
+
return Promise.reject(this.cleanError(error));
|
|
886
|
+
});
|
|
887
|
+
}
|
|
863
888
|
team() {
|
|
864
889
|
return this.client
|
|
865
890
|
.request((0, graphql_request_1.gql) `
|
|
@@ -11,7 +11,7 @@ import { Locale } from '../core/types';
|
|
|
11
11
|
* several parents define is rewritten, as a flat mapping of dotted keys, which is the shape the
|
|
12
12
|
* lookup uses anyway and the one shape where a nested and a dotted key cannot disagree.
|
|
13
13
|
*/
|
|
14
|
-
export declare function mergeInheritedLocales(locales: Array<Locale>): Array<Locale>;
|
|
14
|
+
export declare function mergeInheritedLocales(locales: Array<Locale>, parentTeamSlugs: Array<string>): Array<Locale>;
|
|
15
15
|
/**
|
|
16
16
|
* Locale identifiers are case-sensitive on the platform (pt-BR and pt-br are different locales),
|
|
17
17
|
* but the filesystems macOS and Windows usually run on are not ā both identifiers would collapse
|
|
@@ -22,34 +22,37 @@ const locale_resolver_1 = require("./locale-resolver");
|
|
|
22
22
|
* several parents define is rewritten, as a flat mapping of dotted keys, which is the shape the
|
|
23
23
|
* lookup uses anyway and the one shape where a nested and a dotted key cannot disagree.
|
|
24
24
|
*/
|
|
25
|
-
function mergeInheritedLocales(locales) {
|
|
25
|
+
function mergeInheritedLocales(locales, parentTeamSlugs) {
|
|
26
26
|
const inheritedByIdentifier = new Map();
|
|
27
27
|
for (const locale of locales.filter((locale) => locale.resolvingTeamSlug)) {
|
|
28
28
|
inheritedByIdentifier.set(locale.locale, [...(inheritedByIdentifier.get(locale.locale) ?? []), locale]);
|
|
29
29
|
}
|
|
30
30
|
return [
|
|
31
31
|
...locales.filter((locale) => !locale.resolvingTeamSlug),
|
|
32
|
-
...[...inheritedByIdentifier.values()].map((files) =>
|
|
32
|
+
...[...inheritedByIdentifier.values()].map((files) => files.length === 1 ? files[0] : mergeLocaleFiles(files, parentTeamSlugs)),
|
|
33
33
|
];
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* The platform walks parents
|
|
37
|
-
* the first defining a key wins. Deriving that order here
|
|
38
|
-
* happened to list the rows in (which
|
|
39
|
-
* is what keeps a local run and the
|
|
36
|
+
* The platform walks parents in the team's configured order ā `Team.parentTeamIdsInResolutionOrder`
|
|
37
|
+
* ā and the first defining a key wins. Deriving that order here from the cloud team's
|
|
38
|
+
* `parentTeamSlugs`, rather than trusting the order the API happened to list the rows in (which
|
|
39
|
+
* follows the listing's `orderBy`, `updatedAt` by default), is what keeps a local run and the
|
|
40
|
+
* cloud resolving the same text.
|
|
40
41
|
*/
|
|
41
|
-
function inResolutionOrder(files) {
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
return
|
|
45
|
-
}
|
|
42
|
+
function inResolutionOrder(files, parentTeamSlugs) {
|
|
43
|
+
const precedence = (file) => {
|
|
44
|
+
const index = parentTeamSlugs.indexOf(file.resolvingTeamSlug ?? '');
|
|
45
|
+
return index === -1 ? parentTeamSlugs.length : index;
|
|
46
|
+
};
|
|
47
|
+
return [...files].sort((a, b) => precedence(a) - precedence(b));
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* @param files the same locale as every parent workspace defines it, in any order
|
|
51
|
+
* @param parentTeamSlugs the workspace's parents in inheritance-precedence order
|
|
49
52
|
* @returns one locale carrying their merged translations, attributed to the winning parent
|
|
50
53
|
*/
|
|
51
|
-
function mergeLocaleFiles(files) {
|
|
52
|
-
const ordered = inResolutionOrder(files);
|
|
54
|
+
function mergeLocaleFiles(files, parentTeamSlugs) {
|
|
55
|
+
const ordered = inResolutionOrder(files, parentTeamSlugs);
|
|
53
56
|
const [winner] = ordered;
|
|
54
57
|
const translations = {};
|
|
55
58
|
// Lowest precedence first, so the first parent in resolution order is the text that survives ā
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import FcodeGraphQLClient from './fcode-graphql-client';
|
|
2
|
+
import GlobalConfig from '../config/global';
|
|
3
|
+
declare const TeamAccessService: {
|
|
4
|
+
globalConfig: GlobalConfig;
|
|
5
|
+
clientFactory: (options: {
|
|
6
|
+
url: string;
|
|
7
|
+
accessToken: string;
|
|
8
|
+
teamSlug: string;
|
|
9
|
+
}) => FcodeGraphQLClient;
|
|
10
|
+
ensureTeamAccess(url: string, slug: string): Promise<void>;
|
|
11
|
+
checkAccess(url: string, slug: string): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
export default TeamAccessService;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const auth_service_1 = require("./auth-service");
|
|
4
|
+
const fcode_graphql_client_1 = require("./fcode-graphql-client");
|
|
5
|
+
const global_1 = require("../config/global");
|
|
6
|
+
const errors_1 = require("../core/errors");
|
|
7
|
+
function is403(error) {
|
|
8
|
+
return error?.response?.status === 403;
|
|
9
|
+
}
|
|
10
|
+
const TeamAccessService = {
|
|
11
|
+
globalConfig: new global_1.default(),
|
|
12
|
+
clientFactory: (options) => new fcode_graphql_client_1.default(options),
|
|
13
|
+
// Pre-flight check that the current session can access the team, run before any
|
|
14
|
+
// local state is touched so a denied session fails once with a clear error
|
|
15
|
+
// instead of erroring on every resource pull. A 403 may just mean the token
|
|
16
|
+
// predates the team: groups are snapshotted into the token at issuance
|
|
17
|
+
// (FCT-61231), so re-mint it through the refresh grant and retry once.
|
|
18
|
+
async ensureTeamAccess(url, slug) {
|
|
19
|
+
try {
|
|
20
|
+
await this.checkAccess(url, slug);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!is403(error)) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
if (!(await auth_service_1.default.refreshSession(url))) {
|
|
27
|
+
throw new errors_1.TeamAccessDenied(slug);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await this.checkAccess(url, slug);
|
|
31
|
+
}
|
|
32
|
+
catch (retryError) {
|
|
33
|
+
if (is403(retryError)) {
|
|
34
|
+
throw new errors_1.TeamAccessDenied(slug);
|
|
35
|
+
}
|
|
36
|
+
throw retryError;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async checkAccess(url, slug) {
|
|
41
|
+
const { token } = await this.globalConfig.getAuthInfo(url);
|
|
42
|
+
await this.clientFactory({ url, accessToken: token.access_token, teamSlug: slug }).verifyTeamAccess();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
exports.default = TeamAccessService;
|
package/oclif.manifest.json
CHANGED