@factorialco/fcode-cli 1.5.0 → 2.1.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 +123 -3
- package/dist/commands/http.d.ts +6 -4
- package/dist/commands/http.js +40 -24
- package/dist/commands/setup-debug.js +2 -2
- package/dist/commands/team/aliases/delete.d.ts +9 -0
- package/dist/commands/team/aliases/delete.js +28 -0
- package/dist/commands/team/aliases/list.d.ts +6 -0
- package/dist/commands/team/aliases/list.js +23 -0
- package/dist/commands/team/aliases/set.d.ts +10 -0
- package/dist/commands/team/aliases/set.js +25 -0
- package/dist/commands/team/pull.js +8 -1
- package/dist/commands/team/push.js +9 -1
- package/dist/commands/team/versions/create.d.ts +13 -0
- package/dist/commands/team/versions/create.js +42 -0
- package/dist/commands/team/versions/delete.d.ts +12 -0
- package/dist/commands/team/versions/delete.js +32 -0
- package/dist/commands/team/versions/list.d.ts +6 -0
- package/dist/commands/team/versions/list.js +25 -0
- package/dist/commands/variables/status.js +11 -0
- package/dist/config/settings.d.ts +1 -0
- package/dist/config/settings.js +6 -0
- package/dist/core/types.d.ts +65 -4
- package/dist/core/types.js +16 -5
- package/dist/env.d.ts +2 -2
- package/dist/env.js +1 -1
- package/dist/repositories/team.d.ts +10 -2
- package/dist/repositories/team.js +53 -3
- package/dist/repositories/variable.d.ts +24 -0
- package/dist/repositories/variable.js +70 -22
- package/dist/runtime/executor.js +2 -2
- package/dist/runtime/executor.py +2 -2
- package/dist/runtime/libs/python/fcode/__init__.py +2 -2
- package/dist/services/auth-service.d.ts +3 -0
- package/dist/services/auth-service.js +56 -5
- package/dist/services/fcode-graphql-client.d.ts +20 -1
- package/dist/services/fcode-graphql-client.js +239 -49
- package/dist/services/oauth-browser-service.js +1 -0
- package/dist/services/webhook-auth.d.ts +21 -0
- package/dist/services/webhook-auth.js +77 -0
- package/dist/utils/gitignore.d.ts +2 -0
- package/dist/utils/gitignore.js +6 -0
- package/oclif.manifest.json +208 -17
- package/package.json +10 -2
|
@@ -7,6 +7,15 @@ const global_1 = require("../config/global");
|
|
|
7
7
|
const color_1 = require("@oclif/color");
|
|
8
8
|
const AUTH_URL_PATH = '/protocol/openid-connect';
|
|
9
9
|
const AUTH_CLIENT_ID = 'cli';
|
|
10
|
+
// Refresh this long before the access token actually expires, so a token
|
|
11
|
+
// snapshotted at command init doesn't die mid-command.
|
|
12
|
+
const TOKEN_EXPIRY_MARGIN_SECONDS = 300;
|
|
13
|
+
function withExpiresAt(token) {
|
|
14
|
+
if (typeof token.expires_in === 'number') {
|
|
15
|
+
return { ...token, expires_at: Math.floor(Date.now() / 1000) + token.expires_in };
|
|
16
|
+
}
|
|
17
|
+
return token;
|
|
18
|
+
}
|
|
10
19
|
const AuthService = {
|
|
11
20
|
clientId: process.env.FCODE_CLIENT_ID || AUTH_CLIENT_ID,
|
|
12
21
|
async getToken({ authUrl, email, password, clientId, clientSecret }) {
|
|
@@ -44,7 +53,7 @@ const AuthService = {
|
|
|
44
53
|
});
|
|
45
54
|
const token = await response.json();
|
|
46
55
|
if (response.ok) {
|
|
47
|
-
return token;
|
|
56
|
+
return withExpiresAt(token);
|
|
48
57
|
}
|
|
49
58
|
throw token.error_description;
|
|
50
59
|
},
|
|
@@ -64,14 +73,21 @@ const AuthService = {
|
|
|
64
73
|
globalConfig: new global_1.default(),
|
|
65
74
|
async isAuthenticated(url) {
|
|
66
75
|
try {
|
|
67
|
-
const
|
|
76
|
+
const authInfo = await this.globalConfig.getAuthInfo(url);
|
|
77
|
+
const { authUrl, token } = authInfo;
|
|
68
78
|
try {
|
|
79
|
+
if (this.isTokenExpiring(token)) {
|
|
80
|
+
throw new Error('Access token expired or about to expire');
|
|
81
|
+
}
|
|
69
82
|
await this.getUserInfo(authUrl, token);
|
|
70
83
|
}
|
|
71
84
|
catch (error) {
|
|
72
|
-
await this.
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const refreshed = await this.tryRefresh(url, authInfo);
|
|
86
|
+
if (!refreshed) {
|
|
87
|
+
await this.globalConfig.clearAuthInfo(url);
|
|
88
|
+
cli_1.default.log(color_1.default.yellow('🚪 Session expired, please login again'));
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
75
91
|
}
|
|
76
92
|
return true;
|
|
77
93
|
}
|
|
@@ -79,6 +95,41 @@ const AuthService = {
|
|
|
79
95
|
return false;
|
|
80
96
|
}
|
|
81
97
|
},
|
|
98
|
+
isTokenExpiring(token) {
|
|
99
|
+
return token.expires_at !== undefined && Date.now() / 1000 >= token.expires_at - TOKEN_EXPIRY_MARGIN_SECONDS;
|
|
100
|
+
},
|
|
101
|
+
async refreshToken(authUrl, refreshToken) {
|
|
102
|
+
const body = new URLSearchParams({
|
|
103
|
+
client_id: this.clientId,
|
|
104
|
+
grant_type: 'refresh_token',
|
|
105
|
+
refresh_token: refreshToken,
|
|
106
|
+
});
|
|
107
|
+
const response = await (0, node_fetch_1.default)(`${authUrl}${AUTH_URL_PATH}/token`, {
|
|
108
|
+
body: body.toString(),
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
111
|
+
},
|
|
112
|
+
method: 'POST',
|
|
113
|
+
});
|
|
114
|
+
const token = await response.json();
|
|
115
|
+
if (response.ok) {
|
|
116
|
+
return withExpiresAt(token);
|
|
117
|
+
}
|
|
118
|
+
throw token.error_description;
|
|
119
|
+
},
|
|
120
|
+
async tryRefresh(url, { authUrl, token, userInfo }) {
|
|
121
|
+
if (!token.refresh_token) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const refreshedToken = await this.refreshToken(authUrl, token.refresh_token);
|
|
126
|
+
await this.globalConfig.setAuthInfo(url, { authUrl, token: refreshedToken, userInfo });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
82
133
|
async login({ url, authUrl, email, password, clientId, clientSecret, accessToken }) {
|
|
83
134
|
const token = accessToken
|
|
84
135
|
? { access_token: accessToken, refresh_token: '' }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GraphQLClient } from 'graphql-request';
|
|
2
|
-
import { Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
2
|
+
import { Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
3
3
|
export type FcodeGraphQLClientOptions = {
|
|
4
4
|
url: string;
|
|
5
5
|
accessToken: string;
|
|
@@ -32,8 +32,27 @@ declare class FcodeGraphQLClient {
|
|
|
32
32
|
removeVariable(id: String | undefined): Promise<Process>;
|
|
33
33
|
programmingLanguageManifest(programmingLanguage: ProgrammingLanguage): Promise<ProgrammingLanguageManifest>;
|
|
34
34
|
private inheritedTeam;
|
|
35
|
+
/**
|
|
36
|
+
* The public mode and the default header name are left out, so a public webhook — or one
|
|
37
|
+
* using the default header — keeps the metadata.json it already had on disk.
|
|
38
|
+
*/
|
|
35
39
|
private mapWebhook;
|
|
36
40
|
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug }: any): Process;
|
|
41
|
+
/**
|
|
42
|
+
* GraphQL nests the webhook configuration under `settings`, while team.json and the REST API
|
|
43
|
+
* keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
|
|
44
|
+
* undefined.
|
|
45
|
+
*/
|
|
46
|
+
mapTeamSettings(team: any): RawTeamSettings;
|
|
47
|
+
mapTeamVersion(teamVersion: any): TeamVersion;
|
|
48
|
+
mapTeamVersionAlias(alias: any): TeamVersionAlias;
|
|
49
|
+
allTeamVersions(): Promise<Array<TeamVersion>>;
|
|
50
|
+
createTeamVersion(tag: string, comment?: string): Promise<TeamVersion>;
|
|
51
|
+
deleteTeamVersion(tag: string): Promise<TeamVersion>;
|
|
52
|
+
allTeamVersionAliases(): Promise<Array<TeamVersionAlias>>;
|
|
53
|
+
/** The API upserts: an existing alias with the same name is re-pointed to the tag. */
|
|
54
|
+
createTeamVersionAlias(name: string, tag: string): Promise<TeamVersionAlias>;
|
|
55
|
+
deleteTeamVersionAlias(id: string): Promise<TeamVersionAlias>;
|
|
37
56
|
team(): Promise<RawTeamSettings>;
|
|
38
57
|
pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
|
|
39
58
|
mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
|
|
@@ -3,6 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const graphql_request_1 = require("graphql-request");
|
|
4
4
|
const types_1 = require("../core/types");
|
|
5
5
|
const _ = require("lodash");
|
|
6
|
+
// Shared team version selections so every query/mutation returns the same shape.
|
|
7
|
+
const TEAM_VERSION_FIELDS = `
|
|
8
|
+
id
|
|
9
|
+
tag
|
|
10
|
+
comment
|
|
11
|
+
createdAt
|
|
12
|
+
createdBy {
|
|
13
|
+
id
|
|
14
|
+
name
|
|
15
|
+
}
|
|
16
|
+
manifest {
|
|
17
|
+
resourceType
|
|
18
|
+
resourceId
|
|
19
|
+
slug
|
|
20
|
+
outcome
|
|
21
|
+
error
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
const TEAM_VERSION_ALIAS_FIELDS = `
|
|
25
|
+
id
|
|
26
|
+
name
|
|
27
|
+
tag
|
|
28
|
+
createdAt
|
|
29
|
+
updatedAt
|
|
30
|
+
`;
|
|
6
31
|
// Shared process selection so pull queries and push mutations return the same
|
|
7
32
|
// fields, keeping mapProcess (and therefore contentHash) consistent.
|
|
8
33
|
const PROCESS_FIELDS = `
|
|
@@ -21,15 +46,10 @@ const PROCESS_FIELDS = `
|
|
|
21
46
|
triggers {
|
|
22
47
|
webhook {
|
|
23
48
|
enabled
|
|
49
|
+
authMode
|
|
24
50
|
auth {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
password
|
|
28
|
-
expression {
|
|
29
|
-
username
|
|
30
|
-
password
|
|
31
|
-
}
|
|
32
|
-
}
|
|
51
|
+
headerName
|
|
52
|
+
variableKey
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
}
|
|
@@ -44,6 +64,7 @@ const PROCESS_FIELDS = `
|
|
|
44
64
|
formSettings {
|
|
45
65
|
enabled
|
|
46
66
|
appRole
|
|
67
|
+
authMode
|
|
47
68
|
}
|
|
48
69
|
dependencies {
|
|
49
70
|
scopedToProcess
|
|
@@ -240,33 +261,46 @@ class FcodeGraphQLClient {
|
|
|
240
261
|
return Promise.reject(this.cleanError(error));
|
|
241
262
|
});
|
|
242
263
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
// Pages through every variable: a workspace lists its own plus the ones inherited from
|
|
265
|
+
// its parent workspaces, so a single page is not enough to be sure nothing is missed.
|
|
266
|
+
async allVariables() {
|
|
267
|
+
const pageSize = 200;
|
|
268
|
+
const variables = [];
|
|
269
|
+
let after = null;
|
|
270
|
+
try {
|
|
271
|
+
do {
|
|
272
|
+
const { teamVariables: { edges, pageInfo }, } = await this.client.request((0, graphql_request_1.gql) `
|
|
273
|
+
query teamVariables($first: Int!, $after: String) {
|
|
274
|
+
teamVariables(first: $first, after: $after) {
|
|
275
|
+
edges {
|
|
276
|
+
node {
|
|
277
|
+
id
|
|
278
|
+
key
|
|
279
|
+
value
|
|
280
|
+
isSensitive
|
|
281
|
+
resolvingTeamSlug
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
pageInfo {
|
|
285
|
+
hasNextPage
|
|
286
|
+
endCursor
|
|
254
287
|
}
|
|
255
|
-
}
|
|
256
|
-
pageInfo {
|
|
257
|
-
hasNextPage
|
|
258
|
-
endCursor
|
|
259
288
|
}
|
|
260
289
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
290
|
+
`, {
|
|
291
|
+
after,
|
|
292
|
+
first: pageSize,
|
|
293
|
+
});
|
|
294
|
+
for (const { node: { id, isSensitive, key, value, resolvingTeamSlug }, } of edges) {
|
|
295
|
+
variables.push(new types_1.Variable(id, key, value, isSensitive, this.inheritedTeam(resolvingTeamSlug)));
|
|
296
|
+
}
|
|
297
|
+
after = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
|
|
298
|
+
} while (after);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
268
301
|
return Promise.reject(this.cleanError(error));
|
|
269
|
-
}
|
|
302
|
+
}
|
|
303
|
+
return variables;
|
|
270
304
|
}
|
|
271
305
|
toProcessInput(process, includeCreateOnly) {
|
|
272
306
|
const settings = {
|
|
@@ -277,6 +311,9 @@ class FcodeGraphQLClient {
|
|
|
277
311
|
if (process.form.appRole) {
|
|
278
312
|
settings.formSettings.appRole = process.form.appRole;
|
|
279
313
|
}
|
|
314
|
+
if (process.form.authMode) {
|
|
315
|
+
settings.formSettings.authMode = process.form.authMode;
|
|
316
|
+
}
|
|
280
317
|
}
|
|
281
318
|
const input = {
|
|
282
319
|
name: process.name,
|
|
@@ -292,11 +329,17 @@ class FcodeGraphQLClient {
|
|
|
292
329
|
}
|
|
293
330
|
if (process.webhook) {
|
|
294
331
|
const webhook = { enabled: process.webhook.enabled };
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
332
|
+
// Forwarded including an explicit NONE, so a webhook can be un-protected from
|
|
333
|
+
// metadata.json; omitting the mode leaves the stored one in place.
|
|
334
|
+
if (process.webhook.authMode !== undefined) {
|
|
335
|
+
webhook.authMode = process.webhook.authMode;
|
|
336
|
+
if (process.webhook.auth) {
|
|
337
|
+
const auth = { variableKey: process.webhook.auth.variableKey };
|
|
338
|
+
if (process.webhook.auth.headerName !== undefined) {
|
|
339
|
+
auth.headerName = process.webhook.auth.headerName;
|
|
340
|
+
}
|
|
341
|
+
webhook.auth = auth;
|
|
342
|
+
}
|
|
300
343
|
}
|
|
301
344
|
input.webhook = webhook;
|
|
302
345
|
}
|
|
@@ -532,23 +575,24 @@ class FcodeGraphQLClient {
|
|
|
532
575
|
inheritedTeam(resolvingTeamSlug) {
|
|
533
576
|
return resolvingTeamSlug && resolvingTeamSlug !== this.teamSlug ? resolvingTeamSlug : undefined;
|
|
534
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* The public mode and the default header name are left out, so a public webhook — or one
|
|
580
|
+
* using the default header — keeps the metadata.json it already had on disk.
|
|
581
|
+
*/
|
|
535
582
|
mapWebhook(triggers) {
|
|
536
583
|
const webhookTrigger = triggers?.webhook;
|
|
537
584
|
if (!webhookTrigger) {
|
|
538
585
|
return undefined;
|
|
539
586
|
}
|
|
540
587
|
const webhook = { enabled: Boolean(webhookTrigger.enabled) };
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
webhook.
|
|
549
|
-
}
|
|
550
|
-
if (password != null) {
|
|
551
|
-
webhook.password = password;
|
|
588
|
+
if (webhookTrigger.authMode && webhookTrigger.authMode !== 'NONE') {
|
|
589
|
+
webhook.authMode = webhookTrigger.authMode;
|
|
590
|
+
if (webhookTrigger.auth?.variableKey) {
|
|
591
|
+
const auth = { variableKey: webhookTrigger.auth.variableKey };
|
|
592
|
+
if (webhookTrigger.auth.headerName && webhookTrigger.auth.headerName !== types_1.DEFAULT_WEBHOOK_AUTH_HEADER) {
|
|
593
|
+
auth.headerName = webhookTrigger.auth.headerName;
|
|
594
|
+
}
|
|
595
|
+
webhook.auth = auth;
|
|
552
596
|
}
|
|
553
597
|
}
|
|
554
598
|
return webhook;
|
|
@@ -570,9 +614,143 @@ class FcodeGraphQLClient {
|
|
|
570
614
|
if (settings.formSettings.appRole && settings.formSettings.appRole !== 'NONE') {
|
|
571
615
|
metadata.form.appRole = settings.formSettings.appRole;
|
|
572
616
|
}
|
|
617
|
+
if (settings.formSettings.authMode && settings.formSettings.authMode !== 'NONE') {
|
|
618
|
+
metadata.form.authMode = settings.formSettings.authMode;
|
|
619
|
+
}
|
|
573
620
|
}
|
|
574
621
|
return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug));
|
|
575
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* GraphQL nests the webhook configuration under `settings`, while team.json and the REST API
|
|
625
|
+
* keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
|
|
626
|
+
* undefined.
|
|
627
|
+
*/
|
|
628
|
+
mapTeamSettings(team) {
|
|
629
|
+
const settings = {
|
|
630
|
+
slug: team.slug,
|
|
631
|
+
zoneId: team.zoneId,
|
|
632
|
+
errorHandlerConfig: team.errorHandlerConfig,
|
|
633
|
+
parentTeamSlugs: team.parentTeamSlugs ?? [],
|
|
634
|
+
};
|
|
635
|
+
const webhookAuth = team.settings?.webhookAuth;
|
|
636
|
+
if (webhookAuth?.variableKey) {
|
|
637
|
+
settings.webhookAuth = { variableKey: webhookAuth.variableKey };
|
|
638
|
+
if (webhookAuth.headerName && webhookAuth.headerName !== types_1.DEFAULT_WEBHOOK_AUTH_HEADER) {
|
|
639
|
+
settings.webhookAuth.headerName = webhookAuth.headerName;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return settings;
|
|
643
|
+
}
|
|
644
|
+
mapTeamVersion(teamVersion) {
|
|
645
|
+
return {
|
|
646
|
+
id: teamVersion.id,
|
|
647
|
+
tag: teamVersion.tag,
|
|
648
|
+
comment: teamVersion.comment ?? null,
|
|
649
|
+
manifest: (teamVersion.manifest ?? []).map((entry) => ({
|
|
650
|
+
resourceType: entry.resourceType,
|
|
651
|
+
resourceId: entry.resourceId,
|
|
652
|
+
slug: entry.slug,
|
|
653
|
+
outcome: entry.outcome,
|
|
654
|
+
error: entry.error ?? null,
|
|
655
|
+
})),
|
|
656
|
+
createdAt: teamVersion.createdAt,
|
|
657
|
+
createdBy: teamVersion.createdBy ?? null,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
mapTeamVersionAlias(alias) {
|
|
661
|
+
return {
|
|
662
|
+
id: alias.id,
|
|
663
|
+
name: alias.name,
|
|
664
|
+
tag: alias.tag,
|
|
665
|
+
createdAt: alias.createdAt,
|
|
666
|
+
updatedAt: alias.updatedAt ?? null,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
allTeamVersions() {
|
|
670
|
+
return this.client
|
|
671
|
+
.request((0, graphql_request_1.gql) `
|
|
672
|
+
query allTeamVersions {
|
|
673
|
+
allTeamVersions {
|
|
674
|
+
${TEAM_VERSION_FIELDS}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
`)
|
|
678
|
+
.then(({ allTeamVersions }) => allTeamVersions.map((teamVersion) => this.mapTeamVersion(teamVersion)))
|
|
679
|
+
.catch((error) => {
|
|
680
|
+
return Promise.reject(this.cleanError(error));
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
createTeamVersion(tag, comment) {
|
|
684
|
+
return this.client
|
|
685
|
+
.request((0, graphql_request_1.gql) `
|
|
686
|
+
mutation createTeamVersion($tag: String!, $comment: String) {
|
|
687
|
+
createTeamVersion(tag: $tag, comment: $comment) {
|
|
688
|
+
${TEAM_VERSION_FIELDS}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
`, { tag, comment: comment ?? null })
|
|
692
|
+
.then(({ createTeamVersion }) => this.mapTeamVersion(createTeamVersion))
|
|
693
|
+
.catch((error) => {
|
|
694
|
+
return Promise.reject(this.cleanError(error));
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
deleteTeamVersion(tag) {
|
|
698
|
+
return this.client
|
|
699
|
+
.request((0, graphql_request_1.gql) `
|
|
700
|
+
mutation deleteTeamVersion($tag: String!) {
|
|
701
|
+
deleteTeamVersion(tag: $tag) {
|
|
702
|
+
${TEAM_VERSION_FIELDS}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
`, { tag })
|
|
706
|
+
.then(({ deleteTeamVersion }) => this.mapTeamVersion(deleteTeamVersion))
|
|
707
|
+
.catch((error) => {
|
|
708
|
+
return Promise.reject(this.cleanError(error));
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
allTeamVersionAliases() {
|
|
712
|
+
return this.client
|
|
713
|
+
.request((0, graphql_request_1.gql) `
|
|
714
|
+
query allTeamVersionAliases {
|
|
715
|
+
allTeamVersionAliases {
|
|
716
|
+
${TEAM_VERSION_ALIAS_FIELDS}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
`)
|
|
720
|
+
.then(({ allTeamVersionAliases }) => allTeamVersionAliases.map((alias) => this.mapTeamVersionAlias(alias)))
|
|
721
|
+
.catch((error) => {
|
|
722
|
+
return Promise.reject(this.cleanError(error));
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/** The API upserts: an existing alias with the same name is re-pointed to the tag. */
|
|
726
|
+
createTeamVersionAlias(name, tag) {
|
|
727
|
+
return this.client
|
|
728
|
+
.request((0, graphql_request_1.gql) `
|
|
729
|
+
mutation createTeamVersionAlias($input: TeamVersionAliasInput!) {
|
|
730
|
+
createTeamVersionAlias(input: $input) {
|
|
731
|
+
${TEAM_VERSION_ALIAS_FIELDS}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
`, { input: { name, tag } })
|
|
735
|
+
.then(({ createTeamVersionAlias }) => this.mapTeamVersionAlias(createTeamVersionAlias))
|
|
736
|
+
.catch((error) => {
|
|
737
|
+
return Promise.reject(this.cleanError(error));
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
deleteTeamVersionAlias(id) {
|
|
741
|
+
return this.client
|
|
742
|
+
.request((0, graphql_request_1.gql) `
|
|
743
|
+
mutation deleteTeamVersionAlias($id: ID!) {
|
|
744
|
+
deleteTeamVersionAlias(id: $id) {
|
|
745
|
+
${TEAM_VERSION_ALIAS_FIELDS}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
`, { id })
|
|
749
|
+
.then(({ deleteTeamVersionAlias }) => this.mapTeamVersionAlias(deleteTeamVersionAlias))
|
|
750
|
+
.catch((error) => {
|
|
751
|
+
return Promise.reject(this.cleanError(error));
|
|
752
|
+
});
|
|
753
|
+
}
|
|
576
754
|
team() {
|
|
577
755
|
return this.client
|
|
578
756
|
.request((0, graphql_request_1.gql) `
|
|
@@ -585,10 +763,16 @@ class FcodeGraphQLClient {
|
|
|
585
763
|
tag
|
|
586
764
|
}
|
|
587
765
|
parentTeamSlugs
|
|
766
|
+
settings {
|
|
767
|
+
webhookAuth {
|
|
768
|
+
headerName
|
|
769
|
+
variableKey
|
|
770
|
+
}
|
|
771
|
+
}
|
|
588
772
|
}
|
|
589
773
|
}
|
|
590
774
|
`, { slug: this.teamSlug })
|
|
591
|
-
.then(({ team }) => team)
|
|
775
|
+
.then(({ team }) => this.mapTeamSettings(team))
|
|
592
776
|
.catch((error) => {
|
|
593
777
|
return Promise.reject(this.cleanError(error));
|
|
594
778
|
});
|
|
@@ -605,10 +789,16 @@ class FcodeGraphQLClient {
|
|
|
605
789
|
tag
|
|
606
790
|
}
|
|
607
791
|
parentTeamSlugs
|
|
792
|
+
settings {
|
|
793
|
+
webhookAuth {
|
|
794
|
+
headerName
|
|
795
|
+
variableKey
|
|
796
|
+
}
|
|
797
|
+
}
|
|
608
798
|
}
|
|
609
799
|
}
|
|
610
800
|
`, { slug: this.teamSlug, update })
|
|
611
|
-
.then(({ updateTeam: team }) => team)
|
|
801
|
+
.then(({ updateTeam: team }) => this.mapTeamSettings(team))
|
|
612
802
|
.catch((error) => {
|
|
613
803
|
return Promise.reject(this.cleanError(error));
|
|
614
804
|
});
|
|
@@ -144,6 +144,7 @@ async function loginViaBrowser(url, authUrl, clientId) {
|
|
|
144
144
|
const token = {
|
|
145
145
|
access_token: tokenSet.access_token ?? '',
|
|
146
146
|
refresh_token: tokenSet.refresh_token ?? '',
|
|
147
|
+
expires_at: tokenSet.expires_at,
|
|
147
148
|
};
|
|
148
149
|
const userInfo = await auth_service_1.default.getUserInfo(normalizedAuthUrl, token);
|
|
149
150
|
const authInfo = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
import { ProcessWebhook, WebhookAuth } from '../core/types';
|
|
3
|
+
export type WebhookAuthResult = {
|
|
4
|
+
authorized: true;
|
|
5
|
+
} | {
|
|
6
|
+
authorized: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Node lower-cases incoming header names, while a configured name is stored as the user typed it,
|
|
11
|
+
* so the lookup has to be case-insensitive. A repeated header arrives as an array and is rejected
|
|
12
|
+
* rather than joined: a spliced value must never authenticate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findHeader(headers: http.IncomingHttpHeaders, name: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
|
|
17
|
+
* Returns null only when the webhook is public — TEAM without a workspace configuration throws,
|
|
18
|
+
* because treating it as public would silently unprotect a webhook its owner chose to protect.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveEffectiveAuth(webhook: ProcessWebhook | undefined, teamWebhookAuth: WebhookAuth | undefined): WebhookAuth | null;
|
|
21
|
+
export declare function checkWebhookAuth(effectiveAuth: WebhookAuth | null, headers: http.IncomingHttpHeaders, variableValue: string | undefined): WebhookAuthResult;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findHeader = findHeader;
|
|
4
|
+
exports.resolveEffectiveAuth = resolveEffectiveAuth;
|
|
5
|
+
exports.checkWebhookAuth = checkWebhookAuth;
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const types_1 = require("../core/types");
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors the cloud's webhook authentication so `fcode http` accepts and rejects exactly what the
|
|
10
|
+
* platform does. The scheme in the default Authorization header is case-insensitive per RFC 7235;
|
|
11
|
+
* the token is not. A custom header carries the raw value, with no scheme to parse.
|
|
12
|
+
*/
|
|
13
|
+
const BEARER_PATTERN = /^Bearer[ \t]+(\S+)[ \t]*$/i;
|
|
14
|
+
/**
|
|
15
|
+
* Node lower-cases incoming header names, while a configured name is stored as the user typed it,
|
|
16
|
+
* so the lookup has to be case-insensitive. A repeated header arrives as an array and is rejected
|
|
17
|
+
* rather than joined: a spliced value must never authenticate.
|
|
18
|
+
*/
|
|
19
|
+
function findHeader(headers, name) {
|
|
20
|
+
const lowerCased = name.toLowerCase();
|
|
21
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
22
|
+
if (key.toLowerCase() === lowerCased) {
|
|
23
|
+
return typeof value === 'string' ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
|
|
30
|
+
* Returns null only when the webhook is public — TEAM without a workspace configuration throws,
|
|
31
|
+
* because treating it as public would silently unprotect a webhook its owner chose to protect.
|
|
32
|
+
*/
|
|
33
|
+
function resolveEffectiveAuth(webhook, teamWebhookAuth) {
|
|
34
|
+
const authMode = webhook?.authMode;
|
|
35
|
+
if (!authMode || authMode === 'NONE') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (authMode === 'TEAM') {
|
|
39
|
+
if (!teamWebhookAuth) {
|
|
40
|
+
throw new Error('this webhook inherits the workspace webhook auth, which is not set in team.json');
|
|
41
|
+
}
|
|
42
|
+
return teamWebhookAuth;
|
|
43
|
+
}
|
|
44
|
+
if (!webhook?.auth?.variableKey) {
|
|
45
|
+
throw new Error('this webhook uses the CUSTOM auth mode but names no variable in metadata.json');
|
|
46
|
+
}
|
|
47
|
+
return webhook.auth;
|
|
48
|
+
}
|
|
49
|
+
/** Constant-time over equal-length digests, so neither the token nor its length leaks. */
|
|
50
|
+
function matches(expectedToken, givenToken) {
|
|
51
|
+
const digest = (value) => crypto.createHash('sha256').update(value, 'utf8').digest();
|
|
52
|
+
return crypto.timingSafeEqual(digest(expectedToken), digest(givenToken));
|
|
53
|
+
}
|
|
54
|
+
function checkWebhookAuth(effectiveAuth, headers, variableValue) {
|
|
55
|
+
if (effectiveAuth === null) {
|
|
56
|
+
return { authorized: true };
|
|
57
|
+
}
|
|
58
|
+
const headerName = effectiveAuth.headerName ?? types_1.DEFAULT_WEBHOOK_AUTH_HEADER;
|
|
59
|
+
const headerValue = findHeader(headers, headerName);
|
|
60
|
+
if (headerValue === undefined || headerValue.trim().length === 0) {
|
|
61
|
+
return { authorized: false, reason: `the ${headerName} header is missing` };
|
|
62
|
+
}
|
|
63
|
+
let givenToken = headerValue;
|
|
64
|
+
if (headerName.toLowerCase() === types_1.DEFAULT_WEBHOOK_AUTH_HEADER.toLowerCase()) {
|
|
65
|
+
givenToken = BEARER_PATTERN.exec(headerValue)?.[1];
|
|
66
|
+
if (givenToken === undefined) {
|
|
67
|
+
return { authorized: false, reason: `the ${headerName} header is not a bearer token` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (variableValue === undefined) {
|
|
71
|
+
return { authorized: false, reason: `the ${effectiveAuth.variableKey} variable is not defined` };
|
|
72
|
+
}
|
|
73
|
+
if (!matches(variableValue, givenToken)) {
|
|
74
|
+
return { authorized: false, reason: `the value sent in ${headerName} does not match ${effectiveAuth.variableKey}` };
|
|
75
|
+
}
|
|
76
|
+
return { authorized: true };
|
|
77
|
+
}
|
|
@@ -13,5 +13,7 @@ export declare function renderGitignoreWithInherited(existing: string, inherited
|
|
|
13
13
|
* Refreshes the workspace .gitignore so inherited processes/modules (owned by a
|
|
14
14
|
* parent team and pulled read-only) are not tracked by git. Regenerated from the
|
|
15
15
|
* current workspace metadata, so resources that stop being inherited drop out.
|
|
16
|
+
* Inherited variable values live in one file, which is ignored as soon as the
|
|
17
|
+
* workspace inherits any variable.
|
|
16
18
|
*/
|
|
17
19
|
export declare function syncInheritedGitignore(workspaceDir?: string): Promise<void>;
|
package/dist/utils/gitignore.js
CHANGED
|
@@ -6,6 +6,8 @@ exports.syncInheritedGitignore = syncInheritedGitignore;
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const fs = require("fs-extra");
|
|
8
8
|
const workspace_1 = require("../config/workspace");
|
|
9
|
+
const global_1 = require("../config/global");
|
|
10
|
+
const settings_1 = require("../config/settings");
|
|
9
11
|
const filenamify_1 = require("./filenamify");
|
|
10
12
|
const file_utils_1 = require("./file-utils");
|
|
11
13
|
exports.INHERITED_BLOCK_START = '# BEGIN fcode inherited resources (auto-generated, do not edit)';
|
|
@@ -43,9 +45,12 @@ function stripManagedBlock(content) {
|
|
|
43
45
|
* Refreshes the workspace .gitignore so inherited processes/modules (owned by a
|
|
44
46
|
* parent team and pulled read-only) are not tracked by git. Regenerated from the
|
|
45
47
|
* current workspace metadata, so resources that stop being inherited drop out.
|
|
48
|
+
* Inherited variable values live in one file, which is ignored as soon as the
|
|
49
|
+
* workspace inherits any variable.
|
|
46
50
|
*/
|
|
47
51
|
async function syncInheritedGitignore(workspaceDir = process.cwd()) {
|
|
48
52
|
const team = await new workspace_1.default(workspaceDir).getActiveTeam();
|
|
53
|
+
const inheritedVariablesFileName = await new global_1.default().getSetting(settings_1.SettingKeys.INHERITED_VARIABLES_FILE_NAME);
|
|
49
54
|
const inheritedPaths = [
|
|
50
55
|
...Object.values(team.processes)
|
|
51
56
|
.filter((process) => process.resolvingTeamSlug)
|
|
@@ -53,6 +58,7 @@ async function syncInheritedGitignore(workspaceDir = process.cwd()) {
|
|
|
53
58
|
...Object.values(team.modules)
|
|
54
59
|
.filter((module) => module.resolvingTeamSlug)
|
|
55
60
|
.map((module) => `modules/${(0, filenamify_1.default)(module.name)}/`),
|
|
61
|
+
...(Object.values(team.variables ?? {}).some((variable) => variable.resolvingTeamSlug) ? [inheritedVariablesFileName] : []),
|
|
56
62
|
];
|
|
57
63
|
const gitignorePath = path.join(workspaceDir, '.gitignore');
|
|
58
64
|
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|