@teambit/workspace 1.0.974 → 1.0.976
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/dist/{preview-1778178157117.js → preview-1778254115658.js} +2 -2
- package/dist/scope-trust/index.d.ts +2 -0
- package/dist/scope-trust/index.js +33 -0
- package/dist/scope-trust/index.js.map +1 -0
- package/dist/scope-trust/scope-trust.cmd.d.ts +18 -0
- package/dist/scope-trust/scope-trust.cmd.js +109 -0
- package/dist/scope-trust/scope-trust.cmd.js.map +1 -0
- package/dist/scope-trust/scope-trust.d.ts +93 -0
- package/dist/scope-trust/scope-trust.js +262 -0
- package/dist/scope-trust/scope-trust.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js.map +1 -1
- package/dist/workspace-aspects-loader.js +4 -0
- package/dist/workspace-aspects-loader.js.map +1 -1
- package/dist/workspace.main.runtime.js +13 -0
- package/dist/workspace.main.runtime.js.map +1 -1
- package/package.json +38 -37
- package/scope-trust/index.ts +2 -0
- package/scope-trust/scope-trust.cmd.ts +111 -0
- package/scope-trust/scope-trust.ts +260 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teambit/workspace",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.976",
|
|
4
4
|
"homepage": "https://bit.cloud/teambit/workspace/workspace",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "teambit.workspace",
|
|
8
8
|
"name": "workspace",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.976"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"lodash": "4.17.21",
|
|
@@ -23,46 +23,29 @@
|
|
|
23
23
|
"p-map": "4.0.0",
|
|
24
24
|
"detect-indent": "5.0.0",
|
|
25
25
|
"detect-newline": "3.1.0",
|
|
26
|
+
"enquirer": "2.4.1",
|
|
26
27
|
"@react-hook/latest": "1.0.3",
|
|
27
28
|
"classnames": "^2.5.1",
|
|
28
29
|
"pluralize": "8.0.0",
|
|
29
30
|
"reset-css": "5.0.1",
|
|
30
31
|
"@teambit/component-id": "1.2.4",
|
|
31
32
|
"@teambit/harmony": "0.4.7",
|
|
32
|
-
"@teambit/legacy.extension-data": "0.0.113",
|
|
33
|
-
"@teambit/legacy.scope": "0.0.111",
|
|
34
33
|
"@teambit/component-version": "1.0.4",
|
|
35
|
-
"@teambit/legacy.consumer-component": "0.0.112",
|
|
36
|
-
"@teambit/legacy.consumer": "0.0.111",
|
|
37
34
|
"@teambit/bit-error": "0.0.404",
|
|
38
35
|
"@teambit/lane-id": "0.0.312",
|
|
39
|
-
"@teambit/legacy.bit-map": "0.0.168",
|
|
40
36
|
"@teambit/toolbox.fs.last-modified": "0.0.14",
|
|
41
37
|
"@teambit/toolbox.path.path": "0.0.16",
|
|
42
38
|
"@teambit/graph.cleargraph": "0.0.11",
|
|
43
|
-
"@teambit/logger": "0.0.1413",
|
|
44
|
-
"@teambit/cli": "0.0.1320",
|
|
45
39
|
"@teambit/component.ui.component-status-resolver": "0.0.510",
|
|
46
40
|
"@teambit/legacy.constants": "0.0.26",
|
|
47
41
|
"@teambit/harmony.modules.resolved-component": "0.0.513",
|
|
48
|
-
"@teambit/legacy.utils": "0.0.34",
|
|
49
|
-
"@teambit/scope.remotes": "0.0.111",
|
|
50
|
-
"@teambit/config-store": "0.0.200",
|
|
51
|
-
"@teambit/config": "0.0.1495",
|
|
52
42
|
"@teambit/harmony.modules.requireable-component": "0.0.513",
|
|
53
43
|
"@teambit/toolbox.modules.module-resolver": "0.0.19",
|
|
54
|
-
"@teambit/workspace.modules.node-modules-linker": "0.0.341",
|
|
55
|
-
"@teambit/global-config": "0.0.1323",
|
|
56
|
-
"@teambit/legacy.consumer-config": "0.0.111",
|
|
57
|
-
"@teambit/variants": "0.0.1588",
|
|
58
44
|
"@teambit/component-issues": "0.0.171",
|
|
59
|
-
"@teambit/component.sources": "0.0.163",
|
|
60
45
|
"@teambit/dependencies.modules.packages-excluder": "1.0.8",
|
|
61
46
|
"@teambit/git.modules.git-executable": "0.0.27",
|
|
62
47
|
"@teambit/harmony.modules.in-memory-cache": "0.0.30",
|
|
63
48
|
"@teambit/legacy-bit-id": "1.1.3",
|
|
64
|
-
"@teambit/legacy.component-list": "0.0.165",
|
|
65
|
-
"@teambit/legacy.scope-api": "0.0.166",
|
|
66
49
|
"@teambit/toolbox.path.is-path-inside": "0.0.508",
|
|
67
50
|
"@teambit/workspace.modules.match-pattern": "0.0.520",
|
|
68
51
|
"@teambit/component.ui.component-drawer": "0.0.479",
|
|
@@ -107,23 +90,41 @@
|
|
|
107
90
|
"@teambit/workspace.ui.workspace-component-card": "0.0.569",
|
|
108
91
|
"@teambit/explorer.ui.component-card": "0.0.52",
|
|
109
92
|
"@teambit/workspace.ui.empty-workspace": "0.0.509",
|
|
110
|
-
"@teambit/component": "1.0.
|
|
111
|
-
"@teambit/dependency-resolver": "1.0.
|
|
112
|
-
"@teambit/envs": "1.0.
|
|
113
|
-
"@teambit/
|
|
114
|
-
"@teambit/scope": "
|
|
115
|
-
"@teambit/
|
|
116
|
-
"@teambit/
|
|
117
|
-
"@teambit/
|
|
118
|
-
"@teambit/
|
|
119
|
-
"@teambit/
|
|
120
|
-
"@teambit/
|
|
121
|
-
"@teambit/
|
|
122
|
-
"@teambit/
|
|
123
|
-
"@teambit/
|
|
124
|
-
"@teambit/
|
|
125
|
-
"@teambit/
|
|
126
|
-
"@teambit/
|
|
93
|
+
"@teambit/component": "1.0.976",
|
|
94
|
+
"@teambit/dependency-resolver": "1.0.976",
|
|
95
|
+
"@teambit/envs": "1.0.976",
|
|
96
|
+
"@teambit/legacy.extension-data": "0.0.114",
|
|
97
|
+
"@teambit/legacy.scope": "0.0.112",
|
|
98
|
+
"@teambit/legacy.consumer-component": "0.0.113",
|
|
99
|
+
"@teambit/legacy.consumer": "0.0.112",
|
|
100
|
+
"@teambit/legacy.bit-map": "0.0.169",
|
|
101
|
+
"@teambit/logger": "0.0.1414",
|
|
102
|
+
"@teambit/objects": "0.0.483",
|
|
103
|
+
"@teambit/scope": "1.0.976",
|
|
104
|
+
"@teambit/graph": "1.0.976",
|
|
105
|
+
"@teambit/cli": "0.0.1321",
|
|
106
|
+
"@teambit/isolator": "1.0.976",
|
|
107
|
+
"@teambit/component-tree": "1.0.976",
|
|
108
|
+
"@teambit/legacy.utils": "0.0.35",
|
|
109
|
+
"@teambit/watcher": "1.0.976",
|
|
110
|
+
"@teambit/scope.remotes": "0.0.112",
|
|
111
|
+
"@teambit/aspect-loader": "1.0.976",
|
|
112
|
+
"@teambit/config-store": "0.0.201",
|
|
113
|
+
"@teambit/config": "0.0.1496",
|
|
114
|
+
"@teambit/workspace.modules.node-modules-linker": "0.0.342",
|
|
115
|
+
"@teambit/graphql": "1.0.976",
|
|
116
|
+
"@teambit/bundler": "1.0.976",
|
|
117
|
+
"@teambit/global-config": "0.0.1324",
|
|
118
|
+
"@teambit/legacy.consumer-config": "0.0.112",
|
|
119
|
+
"@teambit/ui": "1.0.976",
|
|
120
|
+
"@teambit/variants": "0.0.1589",
|
|
121
|
+
"@teambit/component.sources": "0.0.164",
|
|
122
|
+
"@teambit/legacy.component-list": "0.0.166",
|
|
123
|
+
"@teambit/legacy.scope-api": "0.0.167",
|
|
124
|
+
"@teambit/command-bar": "1.0.976",
|
|
125
|
+
"@teambit/sidebar": "1.0.976",
|
|
126
|
+
"@teambit/pubsub": "1.0.976",
|
|
127
|
+
"@teambit/deprecation": "1.0.976"
|
|
127
128
|
},
|
|
128
129
|
"devDependencies": {
|
|
129
130
|
"@types/lodash": "4.14.165",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Command } from '@teambit/cli';
|
|
2
|
+
import { formatHint, formatItem, formatSection, formatSuccessSummary, formatTitle, joinSections } from '@teambit/cli';
|
|
3
|
+
import { BitError } from '@teambit/bit-error';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import type { ScopeTrust } from './scope-trust';
|
|
6
|
+
|
|
7
|
+
const ACTIONS = ['list', 'enable', 'disable', 'add', 'remove'] as const;
|
|
8
|
+
type Action = (typeof ACTIONS)[number];
|
|
9
|
+
|
|
10
|
+
export class ScopeTrustCmd implements Command {
|
|
11
|
+
name = 'trust [action] [pattern]';
|
|
12
|
+
description = "manage which scopes are trusted to load aspects (envs, etc.) into the workspace's process";
|
|
13
|
+
arguments = [
|
|
14
|
+
{
|
|
15
|
+
name: 'action',
|
|
16
|
+
description: `one of: ${ACTIONS.join(', ')}. defaults to "list".`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'pattern',
|
|
20
|
+
description: 'scope pattern (required for "add" and "remove")',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
options = [];
|
|
24
|
+
group = 'component-config';
|
|
25
|
+
// Don't load aspects for this command. If the workspace already references
|
|
26
|
+
// an aspect from a scope that the trust list doesn't allow, the pre-command
|
|
27
|
+
// aspect-load step would itself trip the gate, leaving the user with no way
|
|
28
|
+
// to run `bit scope trust` to fix it. Skipping aspect-load keeps the command
|
|
29
|
+
// usable as a recovery path.
|
|
30
|
+
loadAspects = false;
|
|
31
|
+
extendedDescription = `scope-trust is opt-in. when off (the default), aspects from any scope load without a check. when on, aspects from a scope outside the trust list trigger a prompt (interactive shells) or an error (non-interactive).
|
|
32
|
+
|
|
33
|
+
bit scope trust # same as "list"
|
|
34
|
+
bit scope trust list # show status; if on, print the effective trust list
|
|
35
|
+
bit scope trust enable # turn on (writes "trustedScopes": [] to workspace.jsonc)
|
|
36
|
+
bit scope trust disable # turn off (removes "trustedScopes" from workspace.jsonc)
|
|
37
|
+
bit scope trust add PATTERN # add a pattern (auto-enables if needed)
|
|
38
|
+
bit scope trust remove PATTERN # remove a pattern (does NOT disable when list is empty)
|
|
39
|
+
|
|
40
|
+
once on, the effective trust set is: builtin scopes (teambit.*, bitdev.*, and a few others — run "bit scope trust list" to see) + the owner of defaultScope + entries listed under "trustedScopes". patterns are exact ("acme.frontend") or owner wildcard ("acme.*").`;
|
|
41
|
+
|
|
42
|
+
constructor(private scopeTrust: ScopeTrust) {}
|
|
43
|
+
|
|
44
|
+
async report(args: string[]): Promise<string> {
|
|
45
|
+
const [rawAction, pattern] = args;
|
|
46
|
+
const action = (rawAction || 'list') as Action;
|
|
47
|
+
if (!ACTIONS.includes(action)) {
|
|
48
|
+
throw new BitError(`unknown action "${rawAction}". valid actions: ${ACTIONS.join(', ')}.`);
|
|
49
|
+
}
|
|
50
|
+
switch (action) {
|
|
51
|
+
case 'list':
|
|
52
|
+
return this.formatList();
|
|
53
|
+
case 'enable':
|
|
54
|
+
await this.scopeTrust.enable();
|
|
55
|
+
return formatSuccessSummary('scope-trust enabled (added trustedScopes: [] to workspace.jsonc)');
|
|
56
|
+
case 'disable':
|
|
57
|
+
await this.scopeTrust.disable();
|
|
58
|
+
return formatSuccessSummary('scope-trust disabled (removed trustedScopes from workspace.jsonc)');
|
|
59
|
+
case 'add': {
|
|
60
|
+
const p = requirePattern(action, pattern);
|
|
61
|
+
await this.scopeTrust.addTrustedScope(p);
|
|
62
|
+
return formatSuccessSummary(`added ${chalk.bold(p)} to trustedScopes in workspace.jsonc`);
|
|
63
|
+
}
|
|
64
|
+
case 'remove': {
|
|
65
|
+
const p = requirePattern(action, pattern);
|
|
66
|
+
await this.scopeTrust.removeTrustedScope(p);
|
|
67
|
+
return formatSuccessSummary(`removed ${chalk.bold(p)} from trustedScopes in workspace.jsonc`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private formatList(): string {
|
|
73
|
+
if (!this.scopeTrust.isOptedIn()) {
|
|
74
|
+
return joinSections([
|
|
75
|
+
formatTitle('scope-trust is off for this workspace.'),
|
|
76
|
+
'aspects from any scope load without a check.',
|
|
77
|
+
formatHint(
|
|
78
|
+
'to turn on:\n bit scope trust enable (no scopes added; only builtins + owner-of-defaultScope auto-trusted)\n bit scope trust add <pattern> (turns on and adds the first scope)'
|
|
79
|
+
),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
const groups = this.scopeTrust.getEffectiveTrustedPatterns();
|
|
83
|
+
return joinSections([
|
|
84
|
+
formatTitle('scope-trust is on. aspects from these scopes load without a prompt:'),
|
|
85
|
+
formatSection(
|
|
86
|
+
'builtin',
|
|
87
|
+
'',
|
|
88
|
+
groups.builtin.map((p) => formatItem(p))
|
|
89
|
+
),
|
|
90
|
+
formatSection(
|
|
91
|
+
'inferred from workspace defaultScope',
|
|
92
|
+
'',
|
|
93
|
+
groups.owner.map((p) => formatItem(p))
|
|
94
|
+
),
|
|
95
|
+
groups.configured.length
|
|
96
|
+
? formatSection(
|
|
97
|
+
'configured in workspace.jsonc',
|
|
98
|
+
'',
|
|
99
|
+
groups.configured.map((p) => formatItem(p))
|
|
100
|
+
)
|
|
101
|
+
: formatHint('no scopes configured in workspace.jsonc. add one with `bit scope trust add <pattern>`.'),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requirePattern(action: Action, pattern: string | undefined): string {
|
|
107
|
+
if (!pattern) {
|
|
108
|
+
throw new BitError(`"${action}" requires a pattern. example: bit scope trust ${action} acme.frontend`);
|
|
109
|
+
}
|
|
110
|
+
return pattern;
|
|
111
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { ComponentID } from '@teambit/component-id';
|
|
2
|
+
import type { Logger } from '@teambit/logger';
|
|
3
|
+
import { BitError } from '@teambit/bit-error';
|
|
4
|
+
import { isValidScopeName } from '@teambit/legacy-bit-id';
|
|
5
|
+
import { prompt } from 'enquirer';
|
|
6
|
+
import type { Workspace } from '../workspace';
|
|
7
|
+
|
|
8
|
+
const BUILTIN_TRUSTED_PATTERNS = ['teambit.*', 'bitdev.*', 'learn-bit-react.*', 'bitdesign.*', 'frontend.*'];
|
|
9
|
+
|
|
10
|
+
const WORKSPACE_ASPECT_ID = 'teambit.workspace/workspace';
|
|
11
|
+
|
|
12
|
+
const TRUSTED_SCOPES_KEY = 'trustedScopes';
|
|
13
|
+
|
|
14
|
+
export type TrustedScopesGroups = {
|
|
15
|
+
/** patterns built into Bit (e.g. `teambit.*`, `bitdev.*`) */
|
|
16
|
+
builtin: string[];
|
|
17
|
+
/** owner wildcard inferred from `defaultScope` (e.g. `acme.frontend` → `acme.*`) */
|
|
18
|
+
owner: string[];
|
|
19
|
+
/** patterns explicitly configured in `workspace.jsonc` under `trustedScopes` */
|
|
20
|
+
configured: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Workspace-level scope-trust policy. Opt-in: when the `trustedScopes` key is
|
|
25
|
+
* present in workspace.jsonc (even as an empty array), the aspect-load gate
|
|
26
|
+
* is active. When the key is absent, no gate runs and any aspect loads.
|
|
27
|
+
*
|
|
28
|
+
* Once opted in, a scope is trusted if it matches any pattern in:
|
|
29
|
+
* - the builtin set (e.g. `teambit.*`, `bitdev.*`; see `BUILTIN_TRUSTED_PATTERNS`),
|
|
30
|
+
* - the pattern derived from the workspace's `defaultScope`
|
|
31
|
+
* (e.g. `acme.frontend` → `acme.*`; legacy dotless `my-scope` → `my-scope`),
|
|
32
|
+
* - the `trustedScopes` array configured in workspace.jsonc.
|
|
33
|
+
*
|
|
34
|
+
* Patterns are exact (`acme.frontend`) or owner wildcard (`acme.*`).
|
|
35
|
+
*
|
|
36
|
+
* Wired into `ScopeMain` via `setAspectLoadGuard`; the guard runs in the
|
|
37
|
+
* aspect-loader path so untrusted aspects never reach `require()`.
|
|
38
|
+
*/
|
|
39
|
+
export class ScopeTrust {
|
|
40
|
+
private deniedThisRun = new Set<string>();
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
private workspace: Workspace,
|
|
44
|
+
private logger: Logger
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `true` when the workspace has opted in (the `trustedScopes` key is present
|
|
49
|
+
* in workspace.jsonc, even as an empty array). When `false`, the aspect-load
|
|
50
|
+
* gate is a no-op.
|
|
51
|
+
*/
|
|
52
|
+
isOptedIn(): boolean {
|
|
53
|
+
return Object.prototype.hasOwnProperty.call(this.readExt(), TRUSTED_SCOPES_KEY);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Effective trust list, broken down by source. Useful for both internal
|
|
58
|
+
* checks and the `bit scope trust list` UX.
|
|
59
|
+
*/
|
|
60
|
+
getEffectiveTrustedPatterns(): TrustedScopesGroups {
|
|
61
|
+
const ext = this.readExt();
|
|
62
|
+
const configured = Array.isArray(ext[TRUSTED_SCOPES_KEY]) ? (ext[TRUSTED_SCOPES_KEY] as string[]).slice() : [];
|
|
63
|
+
const owner = this.getInferredOwnerPattern();
|
|
64
|
+
return {
|
|
65
|
+
builtin: BUILTIN_TRUSTED_PATTERNS.slice(),
|
|
66
|
+
owner: owner ? [owner] : [],
|
|
67
|
+
configured,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* True iff `scopeName` matches any pattern in the effective trust list.
|
|
73
|
+
* `scopeName` is expected to be the bare scope (e.g. `acme.frontend`).
|
|
74
|
+
*/
|
|
75
|
+
isScopeTrusted(scopeName: string): boolean {
|
|
76
|
+
if (!scopeName) return false;
|
|
77
|
+
const groups = this.getEffectiveTrustedPatterns();
|
|
78
|
+
const all = [...groups.builtin, ...groups.owner, ...groups.configured];
|
|
79
|
+
return all.some((pattern) => ScopeTrust.matchesPattern(scopeName, pattern));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Pattern matcher. Two forms:
|
|
84
|
+
* - exact: `acme.frontend` matches only `acme.frontend`.
|
|
85
|
+
* - owner wildcard: `acme.*` matches `acme.<anything>`.
|
|
86
|
+
*/
|
|
87
|
+
static matchesPattern(scopeName: string, pattern: string): boolean {
|
|
88
|
+
if (pattern === scopeName) return true;
|
|
89
|
+
if (pattern.endsWith('.*')) {
|
|
90
|
+
const owner = pattern.slice(0, -2);
|
|
91
|
+
return scopeName.startsWith(`${owner}.`);
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Opt the workspace in by writing `trustedScopes: []` (idempotent). */
|
|
97
|
+
async enable(): Promise<void> {
|
|
98
|
+
if (this.isOptedIn()) return;
|
|
99
|
+
await this.writeExtPatch({ [TRUSTED_SCOPES_KEY]: [] }, 'enable scope-trust');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Opt the workspace out by removing the `trustedScopes` key (idempotent).
|
|
104
|
+
* Uses `overrideExisting` because key deletion isn't expressible via
|
|
105
|
+
* `mergeIntoExisting`; comments on other keys may be reformatted as a result.
|
|
106
|
+
*/
|
|
107
|
+
async disable(): Promise<void> {
|
|
108
|
+
if (!this.isOptedIn()) return;
|
|
109
|
+
const updated = { ...this.readExt() };
|
|
110
|
+
delete updated[TRUSTED_SCOPES_KEY];
|
|
111
|
+
const wsConfig = this.workspace.getWorkspaceConfig();
|
|
112
|
+
wsConfig.setExtension(WORKSPACE_ASPECT_ID, updated, { overrideExisting: true, ignoreVersion: true });
|
|
113
|
+
await wsConfig.write({ reasonForChange: 'disable scope-trust' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Add `pattern` to `trustedScopes` (auto-enables if not yet). */
|
|
117
|
+
async addTrustedScope(pattern: string): Promise<void> {
|
|
118
|
+
if (!ScopeTrust.isValidPattern(pattern)) {
|
|
119
|
+
throw new BitError(
|
|
120
|
+
`invalid scope pattern: "${pattern}". use an exact scope name (e.g. "acme.frontend" or "my-scope") or an owner wildcard (e.g. "acme.*").`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
await this.mutateConfiguredList(
|
|
124
|
+
(list) => (list.includes(pattern) ? null : [...list, pattern]),
|
|
125
|
+
`add trusted scope ${pattern}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Remove `pattern` from `trustedScopes`. Leaves the key in place even if
|
|
131
|
+
* the list becomes empty — use `disable()` to fully turn the gate off.
|
|
132
|
+
*/
|
|
133
|
+
async removeTrustedScope(pattern: string): Promise<void> {
|
|
134
|
+
await this.mutateConfiguredList(
|
|
135
|
+
(list) => (list.includes(pattern) ? list.filter((p) => p !== pattern) : null),
|
|
136
|
+
`remove trusted scope ${pattern}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the aspect-load guard. No-op when not opted in. When opted in:
|
|
142
|
+
* untrusted scopes get a TTY prompt to extend the trust list, or in
|
|
143
|
+
* non-TTY contexts an instructional error.
|
|
144
|
+
*/
|
|
145
|
+
createGuard(): (componentId: ComponentID) => Promise<void> {
|
|
146
|
+
return async (componentId: ComponentID) => {
|
|
147
|
+
if (!this.isOptedIn()) return;
|
|
148
|
+
const scopeName = componentId.scope;
|
|
149
|
+
if (this.isScopeTrusted(scopeName)) return;
|
|
150
|
+
|
|
151
|
+
const deny = (): never => {
|
|
152
|
+
throw makeUntrustedError(scopeName, componentId);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// The user's answer is persisted to workspace.jsonc on accept; remember
|
|
156
|
+
// a denial so we don't re-prompt for the same scope in this run.
|
|
157
|
+
if (this.deniedThisRun.has(scopeName)) deny();
|
|
158
|
+
|
|
159
|
+
const isInteractive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
160
|
+
if (!isInteractive) deny();
|
|
161
|
+
|
|
162
|
+
const accepted = await this.promptForTrust(scopeName, componentId);
|
|
163
|
+
if (!accepted) {
|
|
164
|
+
this.deniedThisRun.add(scopeName);
|
|
165
|
+
deny();
|
|
166
|
+
}
|
|
167
|
+
await this.addTrustedScope(scopeName);
|
|
168
|
+
this.logger.consoleSuccess(`added "${scopeName}" to trustedScopes in workspace.jsonc`);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private readExt(): Record<string, unknown> {
|
|
173
|
+
try {
|
|
174
|
+
return (this.workspace.getWorkspaceConfig().extension(WORKSPACE_ASPECT_ID, true) || {}) as Record<
|
|
175
|
+
string,
|
|
176
|
+
unknown
|
|
177
|
+
>;
|
|
178
|
+
} catch {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Apply `mutator` to the current `trustedScopes` list. If the mutator
|
|
185
|
+
* returns `null`, treat the call as a no-op (idempotent fast path).
|
|
186
|
+
* Uses `mergeIntoExisting` so other keys' comments are preserved.
|
|
187
|
+
*/
|
|
188
|
+
private async mutateConfiguredList(mutator: (list: string[]) => string[] | null, reason: string): Promise<void> {
|
|
189
|
+
const ext = this.readExt();
|
|
190
|
+
const current: string[] = Array.isArray(ext[TRUSTED_SCOPES_KEY]) ? (ext[TRUSTED_SCOPES_KEY] as string[]) : [];
|
|
191
|
+
const next = mutator(current);
|
|
192
|
+
if (next === null) return;
|
|
193
|
+
await this.writeExtPatch({ [TRUSTED_SCOPES_KEY]: next }, reason);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async writeExtPatch(patch: Record<string, unknown>, reason: string): Promise<void> {
|
|
197
|
+
const wsConfig = this.workspace.getWorkspaceConfig();
|
|
198
|
+
wsConfig.setExtension(WORKSPACE_ASPECT_ID, patch, { mergeIntoExisting: true, ignoreVersion: true });
|
|
199
|
+
await wsConfig.write({ reasonForChange: reason });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns the trust pattern derived from the workspace's `defaultScope`:
|
|
204
|
+
* - `acme.frontend` → `acme.*` (owner wildcard)
|
|
205
|
+
* - `my-scope` (legacy dotless) → `my-scope` (exact match)
|
|
206
|
+
* - empty / unset → undefined
|
|
207
|
+
*/
|
|
208
|
+
private getInferredOwnerPattern(): string | undefined {
|
|
209
|
+
const defaultScope = this.workspace.defaultScope;
|
|
210
|
+
if (!defaultScope) return undefined;
|
|
211
|
+
if (!defaultScope.includes('.')) return defaultScope;
|
|
212
|
+
const owner = defaultScope.split('.')[0];
|
|
213
|
+
if (!owner) return undefined;
|
|
214
|
+
return `${owner}.*`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async promptForTrust(scopeName: string, componentId: ComponentID): Promise<boolean> {
|
|
218
|
+
try {
|
|
219
|
+
const response = (await prompt({
|
|
220
|
+
type: 'toggle',
|
|
221
|
+
name: 'trust',
|
|
222
|
+
message:
|
|
223
|
+
`Aspect ${componentId.toString()} comes from scope "${scopeName}", which isn't on your workspace's trusted list.\n` +
|
|
224
|
+
`Trust "${scopeName}" and add it to workspace.jsonc?`,
|
|
225
|
+
enabled: 'Yes',
|
|
226
|
+
disabled: 'No',
|
|
227
|
+
initial: false,
|
|
228
|
+
// The `toggle` prompt's option type isn't exported by enquirer's main
|
|
229
|
+
// typings; cast just the literal so the rest of the call stays typed.
|
|
230
|
+
} as Parameters<typeof prompt>[0])) as { trust: boolean };
|
|
231
|
+
return Boolean(response.trust);
|
|
232
|
+
} catch {
|
|
233
|
+
// user cancelled the prompt (Ctrl+C etc.)
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static isValidPattern(pattern: string): boolean {
|
|
239
|
+
if (!pattern || typeof pattern !== 'string') return false;
|
|
240
|
+
if (pattern.endsWith('.*')) {
|
|
241
|
+
const owner = pattern.slice(0, -2);
|
|
242
|
+
// wildcard must be a single owner segment ("acme.*"), not nested
|
|
243
|
+
// ("acme.frontend.*") — the matcher only consults scope owners.
|
|
244
|
+
if (owner.includes('.')) return false;
|
|
245
|
+
return isValidScopeName(owner);
|
|
246
|
+
}
|
|
247
|
+
// exact match: "acme.frontend" or dotless legacy "my-scope".
|
|
248
|
+
return isValidScopeName(pattern);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function makeUntrustedError(scopeName: string, componentId: ComponentID): BitError {
|
|
253
|
+
return new BitError(
|
|
254
|
+
`cannot load aspect ${componentId.toString()}: scope "${scopeName}" isn't on the workspace's trusted list.\n` +
|
|
255
|
+
`\n` +
|
|
256
|
+
`to trust this scope, run:\n` +
|
|
257
|
+
` bit scope trust add ${scopeName}\n` +
|
|
258
|
+
`or add it to "trustedScopes" under "${WORKSPACE_ASPECT_ID}" in workspace.jsonc.`
|
|
259
|
+
);
|
|
260
|
+
}
|