@pareto4/pp-core 1.9.0 → 1.11.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/.claude-plugin/plugin.json +1 -1
- package/agents/pp-code-fixer.md +11 -0
- package/agents/pp-code-reviewer.md +77 -1
- package/agents/pp-executor.md +34 -1
- package/agents/pp-planner.md +24 -103
- package/commands/pp-ns-manage.md +2 -1
- package/commands/pp-vault-promote.md +31 -0
- package/gemini-extension.json +1 -1
- package/package.json +5 -3
- package/pareto-skills/pp-brand/SKILL.md +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +160 -3
- package/pp-core/bin/lib/capability-state.cjs +41 -3
- package/pp-core/bin/lib/check-command-router.cjs +473 -3
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/compose-bindings.cjs +736 -0
- package/pp-core/bin/lib/git-provider.cjs +279 -0
- package/pp-core/bin/lib/init-contract.cjs +989 -0
- package/pp-core/bin/lib/init-interactive.cjs +183 -0
- package/pp-core/bin/lib/io.cjs +9 -0
- package/pp-core/bin/lib/org-profile.cjs +696 -0
- package/pp-core/bin/lib/platform-bindings.cjs +268 -64
- package/pp-core/bin/lib/practices-review-check.cjs +938 -0
- package/pp-core/bin/lib/vault-command-router.cjs +239 -2
- package/pp-core/bin/lib/vault-coordinates.cjs +485 -0
- package/pp-core/bin/lib/vault-practices.cjs +982 -0
- package/pp-core/bin/lib/vault-write-back.cjs +842 -0
- package/pp-core/bin/lib/vault.cjs +170 -51
- package/pp-core/bin/pp-tools.cjs +225 -2
- package/pp-core/references/planner-checkpoints.md +72 -0
- package/pp-core/references/planner-tdd.md +55 -0
- package/pp-core/references/planner-vault-practices.md +67 -0
- package/pp-core/templates/practices/README.md +122 -0
- package/pp-core/templates/practices/error-handling.md +18 -0
- package/pp-core/templates/practices/minimal-comments.md +14 -0
- package/pp-core/templates/practices/naming-convention.md +18 -0
- package/pp-core/templates/practices/practice-template.md +23 -0
- package/pp-core/workflows/code-review.md +74 -18
- package/pp-core/workflows/discovery-sprint.md +1 -1
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -1
- package/pp-core/workflows/init.md +137 -76
- package/pp-core/workflows/ship.md +1 -0
- package/pp-core/workflows/vault-write-back.md +213 -0
- package/scripts/gen-capability-registry.cjs +68 -8
- package/scripts/gen-platform-bindings-schema.cjs +148 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +8 -1
- package/scripts/lint-descriptions.cjs +3 -1
- package/scripts/lint-legacy-dir-name.cjs +5 -0
- package/scripts/lint-test-file-count.allowlist.json +10 -1
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/org-profile.cts — the org profile resolver (milestone v1.11.0, Phase 33-01).
|
|
4
|
+
*
|
|
5
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
6
|
+
* compiles to the git-ignored `pp-core/bin/lib/org-profile.cjs`; do NOT
|
|
7
|
+
* hand-edit the compiled output. Behaviour is locked by
|
|
8
|
+
* `tests/org-profile.test.cjs`.
|
|
9
|
+
*
|
|
10
|
+
* This module resolves an OPTIONAL, operator-named npm package — the "org
|
|
11
|
+
* profile" — that carries a small set of vault/platform coordinates for a
|
|
12
|
+
* single operator's projects. The package name lives entirely in the
|
|
13
|
+
* consuming project's own `.planning/config.json`; pp-core's own source
|
|
14
|
+
* never names one (ORG-01, SC1). An install that configures no such
|
|
15
|
+
* pointer resolves nothing, warns about nothing, and probes no filesystem
|
|
16
|
+
* beyond the one config read every project already pays for (ORG-02).
|
|
17
|
+
*
|
|
18
|
+
* It imports ONLY node builtins (`node:fs`, `node:path`) at the top level —
|
|
19
|
+
* no `./vault.cjs`, `./vault-coordinates.cjs`, `./vault-write-back.cjs`,
|
|
20
|
+
* `./io.cjs`, `./shell-command-projection.cjs`. This is load-bearing: in
|
|
21
|
+
* 33-03 BOTH `src/git-provider.cts` and `src/vault-coordinates.cts` import
|
|
22
|
+
* THIS module at their own top level. If this module imported either of
|
|
23
|
+
* them back, that pair would form a load-time require cycle in the
|
|
24
|
+
* CommonJS graph that `require` loads — the exact hazard the cycle-avoidance
|
|
25
|
+
* comments at `src/git-provider.cts:9-23` and
|
|
26
|
+
* `src/vault-coordinates.cts:22-28` exist to prevent, and the reason both
|
|
27
|
+
* of those files write their own inline `cfgGet` instead of importing one
|
|
28
|
+
* from each other.
|
|
29
|
+
*
|
|
30
|
+
* The one exception is a CALL-TIME lazy `require('./git-provider.cjs')`
|
|
31
|
+
* inside `profileValueDisposition`, executed only when a profile value is
|
|
32
|
+
* actually being validated, never at module load. This is the same
|
|
33
|
+
* lazy-require seam `src/vault-command-router.cts` already uses for
|
|
34
|
+
* `./vault.cjs` and `./vault-write-back.cjs`, and it is safe here precisely
|
|
35
|
+
* because `git-provider.cjs` has no top-level dependency on this module —
|
|
36
|
+
* by the time any value is being validated, that module is either already
|
|
37
|
+
* in the require cache or loads cleanly on its own. D-10 requires reusing
|
|
38
|
+
* the shared shape predicates (`normalizeProvider`, `PROVIDERS`) rather
|
|
39
|
+
* than writing a second regex; the lazy seam is how a zero-import leaf can
|
|
40
|
+
* still do that.
|
|
41
|
+
*
|
|
42
|
+
* This file's own prose and code deliberately never name a concrete org
|
|
43
|
+
* profile package or any client — the pointer is described only as "the
|
|
44
|
+
* npm package name an operator configures". `tests/org-profile.test.cjs`
|
|
45
|
+
* proves this with an automated scan (SC1).
|
|
46
|
+
*/
|
|
47
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.HOST_FORBIDDEN_CHARS = exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS = exports.ORG_PROFILE_SEED_KEYS = exports.ORG_PROFILE_SEED_SECTION = exports.ORG_PROFILE_ALLOWLIST = exports.ORG_PROFILE_DATA_FILE = exports.ORG_PROFILE_CONFIG_KEY = void 0;
|
|
52
|
+
exports.resolveOrgProfileDocument = resolveOrgProfileDocument;
|
|
53
|
+
exports.resolveOrgProfile = resolveOrgProfile;
|
|
54
|
+
exports.readProfileValue = readProfileValue;
|
|
55
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
56
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
57
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
58
|
+
/** The dotted `.planning/config.json` key naming the profile package. Deliberately
|
|
59
|
+
* top-level, NOT nested under `vault.`, because the profile also supplies
|
|
60
|
+
* `platform.host` (D-02). */
|
|
61
|
+
exports.ORG_PROFILE_CONFIG_KEY = 'org.profile';
|
|
62
|
+
/** The fixed filename read from the resolved package's own directory — never the
|
|
63
|
+
* package's `main`/`exports` entry, and never loaded via `require` (D-08). */
|
|
64
|
+
exports.ORG_PROFILE_DATA_FILE = 'org-profile.json';
|
|
65
|
+
/**
|
|
66
|
+
* D-06's fixed allowlist: a profile may contribute exactly these five dotted
|
|
67
|
+
* config keys. Entry order is not contractual; membership is. Anything else
|
|
68
|
+
* present in a profile document is dropped with a bounded warning.
|
|
69
|
+
*/
|
|
70
|
+
exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
71
|
+
'vault.provider',
|
|
72
|
+
'vault.fallback_repo',
|
|
73
|
+
'vault.fallback_ref',
|
|
74
|
+
'vault.fallback_host',
|
|
75
|
+
'platform.host',
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* The single top-level key naming the Phase 34 init-only seed section.
|
|
79
|
+
*
|
|
80
|
+
* This namespace is read ONLY by the Phase 34 bindings composer
|
|
81
|
+
* (`pp-tools query compose-bindings`, invoked by `/pp-init`) — never by
|
|
82
|
+
* `resolveOrgProfile`, `readProfileValue`, or any of their consumers
|
|
83
|
+
* (`resolveProvider`, `resolvePlatformHost`, `resolveSharedCoordinates`). It
|
|
84
|
+
* is deliberately ABSENT from `ORG_PROFILE_ALLOWLIST` (D-01): extending the
|
|
85
|
+
* runtime allowlist to cover it would reopen Phase 33 D-05's threat —
|
|
86
|
+
* `assertVaultWriteTarget` decides by the resolved `(repo, host)` pair, so a
|
|
87
|
+
* key that layers at RUNTIME is a key an `npm update` can silently move
|
|
88
|
+
* under the write guard. A seed value only ever reaches the guard via a
|
|
89
|
+
* file a human ran `/pp-init` to produce, reviewed, and committed — never
|
|
90
|
+
* directly from the package.
|
|
91
|
+
*/
|
|
92
|
+
exports.ORG_PROFILE_SEED_SECTION = 'bindings';
|
|
93
|
+
/**
|
|
94
|
+
* The five dotted leaf keys the seed section may carry, each prefixed with
|
|
95
|
+
* `ORG_PROFILE_SEED_SECTION`. Membership is contractual; entry ORDER is not.
|
|
96
|
+
*
|
|
97
|
+
* This array is a DESCRIPTION of the section's shape, not a validator —
|
|
98
|
+
* value guards (required-vs-optional, host/clone-target forms) live in the
|
|
99
|
+
* Phase 34 bindings composer, not here.
|
|
100
|
+
*/
|
|
101
|
+
exports.ORG_PROFILE_SEED_KEYS = Object.freeze([
|
|
102
|
+
`${exports.ORG_PROFILE_SEED_SECTION}.platform.repo`,
|
|
103
|
+
`${exports.ORG_PROFILE_SEED_SECTION}.client.vaultRepo`,
|
|
104
|
+
`${exports.ORG_PROFILE_SEED_SECTION}.client.vaultRef`,
|
|
105
|
+
`${exports.ORG_PROFILE_SEED_SECTION}.client.vaultHost`,
|
|
106
|
+
`${exports.ORG_PROFILE_SEED_SECTION}.provisioning.group`,
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* D-10's four machine-specific clone-target forms, copied VERBATIM (not
|
|
110
|
+
* re-authored) from `LITERAL_CLONE_TARGET_PATTERNS` in `src/git-provider.cts`
|
|
111
|
+
* — the explicit-relative, parent-relative, home-relative and Windows
|
|
112
|
+
* drive-path forms. These are valid-but-warned when an operator types them
|
|
113
|
+
* (Phase 32 D-04) but REJECTED outright when a profile package supplies
|
|
114
|
+
* them, because a human chose one deliberately and a package did not.
|
|
115
|
+
* `tests/org-profile.test.cjs` pins that every entry here is `===`-identical
|
|
116
|
+
* to a member of the upstream array, so an upstream edit goes red here
|
|
117
|
+
* instead of silently desyncing. The absolute-unix-path form, the
|
|
118
|
+
* scheme-URL form and the scp-style form are deliberately NOT among these
|
|
119
|
+
* four — D-10 names exactly four machine-specific forms, and everything
|
|
120
|
+
* else `isLiteralCloneTarget` accepts stays accepted from a profile.
|
|
121
|
+
*/
|
|
122
|
+
exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS = Object.freeze([
|
|
123
|
+
'^\\.\\/', // explicit relative — ./path
|
|
124
|
+
'^\\.\\.\\/', // parent-relative — ../path
|
|
125
|
+
'^~', // home-relative — ~ or ~/path
|
|
126
|
+
'^[A-Za-z]:[\\\\/]', // Windows drive path — C:\ or C:/
|
|
127
|
+
]);
|
|
128
|
+
// Compiled once at module load, without the global flag (a global regex
|
|
129
|
+
// carries `lastIndex` state across `test` calls and would make the
|
|
130
|
+
// predicate answer differently on repeated identical inputs) — the same
|
|
131
|
+
// convention `src/git-provider.cts` uses for `LITERAL_CLONE_TARGET_PATTERNS`.
|
|
132
|
+
const compiledRejectedCloneTargetPatterns = exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS.map((source) => new RegExp(source));
|
|
133
|
+
/**
|
|
134
|
+
* Characters a profile-supplied host may never contain (T-33-04).
|
|
135
|
+
*
|
|
136
|
+
* `vault.fallback_host` and `platform.host` are interpolated by
|
|
137
|
+
* `composeCloneTarget` as the AUTHORITY of `https://{host}/{repo}`, so this
|
|
138
|
+
* guard has to do more than keep a path out of the value — it has to keep
|
|
139
|
+
* the value from escaping the authority component at all.
|
|
140
|
+
*
|
|
141
|
+
* `@` is the load-bearing entry: `dev.azure.com@attacker.example.com`
|
|
142
|
+
* composes to `https://dev.azure.com@attacker.example.com/…`, which every
|
|
143
|
+
* RFC-3986 parser reads as userinfo `dev.azure.com` at host
|
|
144
|
+
* `attacker.example.com`. Since a profile is dependency-supplied, that is a
|
|
145
|
+
* silent `npm update`-reachable redirect of the clone — and of the
|
|
146
|
+
* `(repo, host)` pair `assertVaultWriteTarget` decides on. `?` and `#`
|
|
147
|
+
* terminate the authority early. `/` and `\` carry a path.
|
|
148
|
+
*
|
|
149
|
+
* `:` is deliberately absent — a port is legitimate for a self-hosted
|
|
150
|
+
* instance and cannot change WHICH host is contacted.
|
|
151
|
+
*/
|
|
152
|
+
exports.HOST_FORBIDDEN_CHARS = Object.freeze([
|
|
153
|
+
'/',
|
|
154
|
+
'\\',
|
|
155
|
+
'@',
|
|
156
|
+
'?',
|
|
157
|
+
'#',
|
|
158
|
+
]);
|
|
159
|
+
const PROFILE_FLATTEN_DEPTH_CAP = 4;
|
|
160
|
+
const PROFILE_FLATTEN_LEAF_CAP = 200;
|
|
161
|
+
const PROFILE_IGNORED_WARNING_CAP = 5;
|
|
162
|
+
// ─── Private helpers (deliberate per-module duplication) ────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Read a dotted config key from `.planning/config.json`, returning
|
|
165
|
+
* `fallback` on any miss/unreadable-file/parse-error/non-object. Never
|
|
166
|
+
* throws. Byte-for-byte mirror of `cfgGet` in `src/vault-coordinates.cts`
|
|
167
|
+
* (lines 114-125), which itself mirrors the canonical original at
|
|
168
|
+
* `src/git-provider.cts` (lines 78-89). Written inline rather than imported
|
|
169
|
+
* — see the module header for why (the cycle this module must not create).
|
|
170
|
+
*/
|
|
171
|
+
function cfgGet(cwd, key, fallback) {
|
|
172
|
+
try {
|
|
173
|
+
const cfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, '.planning', 'config.json'), 'utf-8'));
|
|
174
|
+
const v = key.split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), cfg);
|
|
175
|
+
return v === undefined ? fallback : v;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return fallback;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Mirror of `isObject` in `src/vault-coordinates.cts` (lines 127-129). */
|
|
182
|
+
function isObject(v) {
|
|
183
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Bound a rejected/untrusted config or profile value before it is echoed
|
|
187
|
+
* into a warning. Byte-for-byte mirror of `sanitizeForWarning` in
|
|
188
|
+
* `src/git-provider.cts` (lines 98-100) / `src/vault-coordinates.cts`
|
|
189
|
+
* (lines 155-157): strip CR/LF, collapse internal whitespace, trim,
|
|
190
|
+
* truncate to 40 characters (T-31-05, T-33-05).
|
|
191
|
+
*/
|
|
192
|
+
function sanitizeForWarning(value) {
|
|
193
|
+
return value.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
194
|
+
}
|
|
195
|
+
/** Bound a whole warning string to 200 characters — every warning this module
|
|
196
|
+
* emits goes through this (T-31-05). */
|
|
197
|
+
function bound(warning) {
|
|
198
|
+
return warning.slice(0, 200);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Describe an arbitrary config-supplied value as a string for a warning,
|
|
202
|
+
* without ever calling the ambient `toString`/template-literal coercion on
|
|
203
|
+
* an object (which would silently produce `"[object Object]"`). Strings
|
|
204
|
+
* pass through; numbers/booleans coerce safely; everything else goes
|
|
205
|
+
* through `JSON.stringify`, falling back to `Object.prototype.toString`
|
|
206
|
+
* (an explicit, guaranteed-safe call, not the value's own) on failure.
|
|
207
|
+
*/
|
|
208
|
+
function describeUnknown(value) {
|
|
209
|
+
if (typeof value === 'string')
|
|
210
|
+
return value;
|
|
211
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
212
|
+
return String(value);
|
|
213
|
+
if (value === null)
|
|
214
|
+
return 'null';
|
|
215
|
+
try {
|
|
216
|
+
const serialized = JSON.stringify(value);
|
|
217
|
+
return typeof serialized === 'string' ? serialized : Object.prototype.toString.call(value);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return Object.prototype.toString.call(value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ─── resolveGlobalPackageRoot ────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* D-03's "global" half of the two-path resolution ladder, as a pure
|
|
226
|
+
* heuristic with NO subprocess spawn (this repo avoids spawning a
|
|
227
|
+
* subprocess for a computation that does not strictly require one):
|
|
228
|
+
*
|
|
229
|
+
* 1. `NPM_CONFIG_PREFIX` / `npm_config_prefix`, when set to a non-blank
|
|
230
|
+
* string: `<prefix>/node_modules` on `win32`, `<prefix>/lib/node_modules`
|
|
231
|
+
* otherwise.
|
|
232
|
+
* 2. Otherwise, derived from `process.execPath`: with
|
|
233
|
+
* `execDir = path.dirname(process.execPath)`, `path.join(execDir,
|
|
234
|
+
* 'node_modules')` on `win32`, `path.resolve(execDir, '..', 'lib',
|
|
235
|
+
* 'node_modules')` otherwise.
|
|
236
|
+
*
|
|
237
|
+
* The candidate is returned only when `fs.existsSync(candidate)`;
|
|
238
|
+
* otherwise `null`.
|
|
239
|
+
*
|
|
240
|
+
* Accepted residual: an nvm/Volta-style layout that honours neither
|
|
241
|
+
* `NPM_CONFIG_PREFIX` nor places the global tree beside the node binary
|
|
242
|
+
* will simply not resolve a globally-installed profile — this degrades
|
|
243
|
+
* per D-09 (falls through to the no-profile behaviour with a warning)
|
|
244
|
+
* rather than failing.
|
|
245
|
+
*
|
|
246
|
+
* pp-core's own install directory is deliberately NOT a candidate here:
|
|
247
|
+
* the AI-runtime install is a bare directory copy with no `package.json`
|
|
248
|
+
* and no `node_modules` of its own, so resolving relative to it would
|
|
249
|
+
* never find a sibling package.
|
|
250
|
+
*/
|
|
251
|
+
function resolveGlobalPackageRoot() {
|
|
252
|
+
try {
|
|
253
|
+
const prefixEnv = process.env.NPM_CONFIG_PREFIX ?? process.env.npm_config_prefix;
|
|
254
|
+
let candidate;
|
|
255
|
+
if (typeof prefixEnv === 'string' && prefixEnv.trim() !== '') {
|
|
256
|
+
const prefix = prefixEnv.trim();
|
|
257
|
+
candidate =
|
|
258
|
+
process.platform === 'win32' ? node_path_1.default.join(prefix, 'node_modules') : node_path_1.default.join(prefix, 'lib', 'node_modules');
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const execDir = node_path_1.default.dirname(process.execPath);
|
|
262
|
+
candidate =
|
|
263
|
+
process.platform === 'win32'
|
|
264
|
+
? node_path_1.default.join(execDir, 'node_modules')
|
|
265
|
+
: node_path_1.default.resolve(execDir, '..', 'lib', 'node_modules');
|
|
266
|
+
}
|
|
267
|
+
return node_fs_1.default.existsSync(candidate) ? candidate : null;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// ─── resolveProfilePackage ───────────────────────────────────────────────────
|
|
274
|
+
/**
|
|
275
|
+
* D-03's two-path resolution ladder. Attempt `require.resolve(pkgName +
|
|
276
|
+
* '/package.json', { paths: [cwd] })` first and report `source: 'project'`;
|
|
277
|
+
* on failure attempt the same with `paths: [globalRoot]` when a global root
|
|
278
|
+
* was found and report `source: 'global'`; otherwise `null`. The
|
|
279
|
+
* project-local hit always wins when both would resolve — the global
|
|
280
|
+
* attempt is only ever reached when the project attempt threw.
|
|
281
|
+
*
|
|
282
|
+
* Mirrors `resolveEslintCli` in `src/prohibition-enforcement.cts` (lines
|
|
283
|
+
* 220-228): the same try/catch-returns-null shape, one level removed
|
|
284
|
+
* (resolving a data file's sibling `package.json`, not a CLI entry point).
|
|
285
|
+
*/
|
|
286
|
+
function resolveProfilePackage(pkgName, cwd, globalRoot) {
|
|
287
|
+
try {
|
|
288
|
+
return { pkgJsonPath: require.resolve(`${pkgName}/package.json`, { paths: [cwd] }), source: 'project' };
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// fall through to the global attempt below
|
|
292
|
+
}
|
|
293
|
+
if (globalRoot) {
|
|
294
|
+
try {
|
|
295
|
+
return { pkgJsonPath: require.resolve(`${pkgName}/package.json`, { paths: [globalRoot] }), source: 'global' };
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// fall through to null below
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Flatten a parsed profile document to dotted leaf paths, recursing only
|
|
305
|
+
* through plain objects. Bounded on two axes (T-33-06, denial-of-service):
|
|
306
|
+
* a depth cap of 4 and a visited-leaf cap of 200. A document exceeding
|
|
307
|
+
* either cap stops flattening immediately and reports exactly one bounded
|
|
308
|
+
* warning — the caller never sees more than one such warning regardless of
|
|
309
|
+
* how far over either cap the document goes.
|
|
310
|
+
*/
|
|
311
|
+
function flattenProfileDocument(doc) {
|
|
312
|
+
const leaves = [];
|
|
313
|
+
let cappedWarning = null;
|
|
314
|
+
function walk(obj, prefix, depth) {
|
|
315
|
+
if (cappedWarning)
|
|
316
|
+
return;
|
|
317
|
+
for (const k of Object.keys(obj)) {
|
|
318
|
+
if (cappedWarning)
|
|
319
|
+
return;
|
|
320
|
+
const dotted = prefix ? `${prefix}.${k}` : k;
|
|
321
|
+
const v = obj[k];
|
|
322
|
+
if (isObject(v)) {
|
|
323
|
+
if (depth + 1 > PROFILE_FLATTEN_DEPTH_CAP) {
|
|
324
|
+
cappedWarning = 'org profile document was too deeply nested to inspect fully — stopped flattening.';
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
walk(v, dotted, depth + 1);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
if (leaves.length >= PROFILE_FLATTEN_LEAF_CAP) {
|
|
331
|
+
cappedWarning = 'org profile document was too large to inspect fully — stopped flattening.';
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
leaves.push({ key: dotted, value: v });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
walk(doc, '', 0);
|
|
339
|
+
return { leaves, cappedWarning };
|
|
340
|
+
}
|
|
341
|
+
// ─── profileValueDisposition (D-10) ──────────────────────────────────────────
|
|
342
|
+
/**
|
|
343
|
+
* The D-10 guard: profile-supplied values pass the same scalar guard every
|
|
344
|
+
* allowlisted key gets, then a stricter per-key disposition — expressed as
|
|
345
|
+
* a DISPOSITION over the shared shape predicates, never as a parallel
|
|
346
|
+
* regex (D-10's "one source of shape truth" requirement).
|
|
347
|
+
*
|
|
348
|
+
* Scalar guard, applied to every key: the value must be a string; its
|
|
349
|
+
* trimmed form must be non-empty; the UNTRIMMED value must carry no ASCII
|
|
350
|
+
* control character; the TRIMMED value must carry no whitespace (so
|
|
351
|
+
* leading/trailing padding — e.g. `' azure '` — is tolerated, matching
|
|
352
|
+
* every other config reader in this seam, but an internal space or newline
|
|
353
|
+
* is not); and the trimmed value must not start with `-` (the T-29-26
|
|
354
|
+
* flag-shaped refusal `isLiteralCloneTarget` already applies at
|
|
355
|
+
* `src/git-provider.cts:186`, restated here as a disposition because a
|
|
356
|
+
* plain slug beginning with `-` is not a literal clone target and would
|
|
357
|
+
* otherwise pass `isLiteralCloneTarget` unmolested). The value carried
|
|
358
|
+
* forward past the scalar guard is always the trimmed string.
|
|
359
|
+
*
|
|
360
|
+
* Per-key dispositions:
|
|
361
|
+
* - `vault.provider`: accept only when `normalizeProvider(value)` (obtained
|
|
362
|
+
* via the call-time lazy `require('./git-provider.cjs')`, never a
|
|
363
|
+
* top-level import) returns non-null, and carry forward the NORMALIZED
|
|
364
|
+
* id. Reason on failure names the supported ids via `PROVIDERS`.
|
|
365
|
+
* - `vault.fallback_repo`: reject when the trimmed value matches any of
|
|
366
|
+
* `PROFILE_REJECTED_CLONE_TARGET_PATTERNS`. Everything else — an
|
|
367
|
+
* absolute path, a scheme URL, scp-style `user@host:path`, or a plain
|
|
368
|
+
* `group/project` slug — is accepted UNCHANGED (T-30-08: never rewritten,
|
|
369
|
+
* normalised or stripped).
|
|
370
|
+
* - `vault.fallback_host` / `platform.host`: reject when the trimmed value
|
|
371
|
+
* contains any of `/` `\` `@` `?` `#` — these values are interpolated
|
|
372
|
+
* into a clone URL by `composeCloneTarget` as the AUTHORITY component,
|
|
373
|
+
* so the guard has to keep the value from carrying a path AND from
|
|
374
|
+
* escaping the authority at all. `@` is the load-bearing one: a host of
|
|
375
|
+
* `dev.azure.com@attacker.example.com` composes to
|
|
376
|
+
* `https://dev.azure.com@attacker.example.com/…`, which every RFC-3986
|
|
377
|
+
* parser reads as userinfo `dev.azure.com` at host `attacker.example.com`
|
|
378
|
+
* — a dependency-supplied value silently redirecting the clone, and with
|
|
379
|
+
* it the `(repo, host)` pair `assertVaultWriteTarget` decides on. `?` and
|
|
380
|
+
* `#` likewise terminate the authority early. None is legal in a
|
|
381
|
+
* hostname. `:` is deliberately still allowed: a port is legitimate for
|
|
382
|
+
* a self-hosted instance and cannot change WHICH host is contacted.
|
|
383
|
+
* - `vault.fallback_ref`: the scalar guard alone.
|
|
384
|
+
*
|
|
385
|
+
* Wrapped so that any unexpected fault (including a `require` failure on
|
|
386
|
+
* the lazy seam) degrades to rejecting the value rather than throwing
|
|
387
|
+
* (D-09) — this function never throws.
|
|
388
|
+
*/
|
|
389
|
+
function profileValueDisposition(key, value) {
|
|
390
|
+
try {
|
|
391
|
+
if (typeof value !== 'string') {
|
|
392
|
+
return { ok: false, reason: 'value must be a string' };
|
|
393
|
+
}
|
|
394
|
+
const trimmed = value.trim();
|
|
395
|
+
if (trimmed === '') {
|
|
396
|
+
return { ok: false, reason: 'value must not be blank' };
|
|
397
|
+
}
|
|
398
|
+
if (/[\x00-\x1f\x7f]/.test(value)) {
|
|
399
|
+
return { ok: false, reason: 'value must not contain control characters' };
|
|
400
|
+
}
|
|
401
|
+
if (/\s/.test(trimmed)) {
|
|
402
|
+
return { ok: false, reason: 'value must not contain whitespace' };
|
|
403
|
+
}
|
|
404
|
+
if (trimmed.startsWith('-')) {
|
|
405
|
+
return { ok: false, reason: 'value must not begin with "-"' };
|
|
406
|
+
}
|
|
407
|
+
if (key === 'vault.provider') {
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
409
|
+
const gp = require('./git-provider.cjs');
|
|
410
|
+
const normalized = gp.normalizeProvider(trimmed);
|
|
411
|
+
if (normalized === null) {
|
|
412
|
+
return { ok: false, reason: `unsupported provider — supported: ${gp.PROVIDERS.join(', ')}` };
|
|
413
|
+
}
|
|
414
|
+
return { ok: true, value: normalized };
|
|
415
|
+
}
|
|
416
|
+
if (key === 'vault.fallback_repo') {
|
|
417
|
+
if (compiledRejectedCloneTargetPatterns.some((re) => re.test(trimmed))) {
|
|
418
|
+
return { ok: false, reason: 'a machine-specific path is not accepted from an org profile' };
|
|
419
|
+
}
|
|
420
|
+
return { ok: true, value: trimmed };
|
|
421
|
+
}
|
|
422
|
+
if (key === 'vault.fallback_host' || key === 'platform.host') {
|
|
423
|
+
// Authority-escape guard. `/` and `\` keep a path out of the value;
|
|
424
|
+
// `@` `?` `#` keep the value from redirecting or truncating the
|
|
425
|
+
// authority component it is interpolated into. See the doc block.
|
|
426
|
+
for (const ch of exports.HOST_FORBIDDEN_CHARS) {
|
|
427
|
+
if (trimmed.includes(ch)) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
reason: `a host must not contain ${ch} — it must be a bare host, optionally with a port`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { ok: true, value: trimmed };
|
|
435
|
+
}
|
|
436
|
+
// vault.fallback_ref: the scalar guard alone.
|
|
437
|
+
return { ok: true, value: trimmed };
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return { ok: false, reason: 'value could not be validated' };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ─── resolveOrgProfileDocument (D-01) ────────────────────────────────────────
|
|
444
|
+
/**
|
|
445
|
+
* Resolve the org profile configured (if any) for the project at `cwd`, and
|
|
446
|
+
* return the RAW parsed document — unflattened, unfiltered, including any
|
|
447
|
+
* `ORG_PROFILE_SEED_SECTION` the D-06 allowlist deliberately excludes.
|
|
448
|
+
*
|
|
449
|
+
* This is `resolveOrgProfile`'s own pointer→package→document walk (its
|
|
450
|
+
* former steps 1-3), extracted so the two have exactly ONE implementation of
|
|
451
|
+
* that walk (the WR-01 defect class this repo has already paid for twice) —
|
|
452
|
+
* `resolveOrgProfile` calls this function for its own steps 1-3 rather than
|
|
453
|
+
* duplicating them. Consumers that need the D-06-filtered `contributes` map
|
|
454
|
+
* still want `resolveOrgProfile`; this function exists for the Phase 34
|
|
455
|
+
* bindings composer, which needs the seed section `resolveOrgProfile`
|
|
456
|
+
* deliberately never surfaces.
|
|
457
|
+
*
|
|
458
|
+
* The `_globalRoot` option is the repo's `_`-prefixed test seam convention
|
|
459
|
+
* (as used by `_vault` / `_exec` / `_writeBack` in
|
|
460
|
+
* `src/vault-command-router.cts`); production callers omit it and the
|
|
461
|
+
* module calls `resolveGlobalPackageRoot()`. Passing `_globalRoot`
|
|
462
|
+
* explicitly (including `null`, to simulate "no global root found")
|
|
463
|
+
* overrides the production heuristic for that one call.
|
|
464
|
+
*
|
|
465
|
+
* Never throws (D-09): any fault anywhere in this function degrades to the
|
|
466
|
+
* fully-absent shape plus one bounded warning, exactly as if no profile
|
|
467
|
+
* were configured. The document is read only via `fs.readFileSync` +
|
|
468
|
+
* `JSON.parse`, never `require` (D-08).
|
|
469
|
+
*/
|
|
470
|
+
function resolveOrgProfileDocument(cwd, opts) {
|
|
471
|
+
const absentDocShape = (warnings = []) => ({
|
|
472
|
+
configured: null,
|
|
473
|
+
resolved: false,
|
|
474
|
+
packageName: null,
|
|
475
|
+
version: null,
|
|
476
|
+
source: 'none',
|
|
477
|
+
dataPath: null,
|
|
478
|
+
document: {},
|
|
479
|
+
warnings,
|
|
480
|
+
});
|
|
481
|
+
try {
|
|
482
|
+
// Step 1: read the pointer. An absent key (undefined) or a blank string
|
|
483
|
+
// is the fully-absent shape with ZERO warnings and no further
|
|
484
|
+
// filesystem access — this is the ORG-02 contract. A present
|
|
485
|
+
// non-string value is the one exception: same absent shape, plus
|
|
486
|
+
// exactly one bounded warning.
|
|
487
|
+
const rawPointer = cfgGet(cwd, exports.ORG_PROFILE_CONFIG_KEY, undefined);
|
|
488
|
+
if (rawPointer === undefined) {
|
|
489
|
+
return absentDocShape();
|
|
490
|
+
}
|
|
491
|
+
if (typeof rawPointer !== 'string') {
|
|
492
|
+
const echoed = sanitizeForWarning(describeUnknown(rawPointer));
|
|
493
|
+
return absentDocShape([
|
|
494
|
+
bound(`${exports.ORG_PROFILE_CONFIG_KEY} value "${echoed}" is not a string — ignoring, no org profile applied.`),
|
|
495
|
+
]);
|
|
496
|
+
}
|
|
497
|
+
const pkgName = rawPointer.trim();
|
|
498
|
+
if (pkgName === '') {
|
|
499
|
+
return absentDocShape();
|
|
500
|
+
}
|
|
501
|
+
// Step 2: resolve the package (project, then global).
|
|
502
|
+
const globalRoot = opts && opts._globalRoot !== undefined ? opts._globalRoot : resolveGlobalPackageRoot();
|
|
503
|
+
const hit = resolveProfilePackage(pkgName, cwd, globalRoot);
|
|
504
|
+
if (!hit) {
|
|
505
|
+
const echoed = sanitizeForWarning(pkgName);
|
|
506
|
+
return {
|
|
507
|
+
...absentDocShape(),
|
|
508
|
+
configured: pkgName,
|
|
509
|
+
warnings: [
|
|
510
|
+
bound(`org profile "${echoed}" was not found — resolution was attempted from the project and the global npm root and found nothing. Falling back to built-in defaults.`),
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const packageDir = node_path_1.default.dirname(hit.pkgJsonPath);
|
|
515
|
+
// Step 3: read the package's own version, then its data file. NEVER
|
|
516
|
+
// load the profile package or any file inside it via `require` (D-08) —
|
|
517
|
+
// only require.resolve (path resolution), fs.readFileSync and JSON.parse.
|
|
518
|
+
let version = null;
|
|
519
|
+
try {
|
|
520
|
+
const pkgJson = JSON.parse(node_fs_1.default.readFileSync(hit.pkgJsonPath, 'utf-8'));
|
|
521
|
+
version = isObject(pkgJson) && typeof pkgJson.version === 'string' ? pkgJson.version : null;
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
version = null;
|
|
525
|
+
}
|
|
526
|
+
const dataPath = node_path_1.default.join(packageDir, exports.ORG_PROFILE_DATA_FILE);
|
|
527
|
+
const warnings = [];
|
|
528
|
+
let doc = {};
|
|
529
|
+
try {
|
|
530
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(dataPath, 'utf-8'));
|
|
531
|
+
if (isObject(parsed)) {
|
|
532
|
+
doc = parsed;
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
warnings.push(bound(`org profile "${pkgName}" data file is not a JSON object — no coordinates contributed.`));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
warnings.push(bound(`org profile "${pkgName}" data file could not be read — no coordinates contributed.`));
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
configured: pkgName,
|
|
543
|
+
resolved: true,
|
|
544
|
+
packageName: pkgName,
|
|
545
|
+
version,
|
|
546
|
+
source: hit.source,
|
|
547
|
+
dataPath,
|
|
548
|
+
document: doc,
|
|
549
|
+
warnings,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return absentDocShape(['org profile resolution failed unexpectedly — falling back to built-in defaults.']);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ─── resolveOrgProfile ────────────────────────────────────────────────────────
|
|
557
|
+
/**
|
|
558
|
+
* Resolve the org profile configured (if any) for the project at `cwd`.
|
|
559
|
+
*
|
|
560
|
+
* Delegates its own steps 1-3 (pointer → package → raw document) to
|
|
561
|
+
* `resolveOrgProfileDocument` — see that function's doc comment for the
|
|
562
|
+
* `_globalRoot` test seam and the D-09 never-throws contract, both of which
|
|
563
|
+
* this function inherits unchanged.
|
|
564
|
+
*
|
|
565
|
+
* Never throws (D-09): any fault anywhere in this function degrades to the
|
|
566
|
+
* fully-absent shape plus one bounded warning, exactly as if no profile
|
|
567
|
+
* were configured.
|
|
568
|
+
*/
|
|
569
|
+
function resolveOrgProfile(cwd, opts) {
|
|
570
|
+
const absentShape = (warnings = []) => ({
|
|
571
|
+
configured: null,
|
|
572
|
+
resolved: false,
|
|
573
|
+
packageName: null,
|
|
574
|
+
version: null,
|
|
575
|
+
source: 'none',
|
|
576
|
+
dataPath: null,
|
|
577
|
+
contributes: {},
|
|
578
|
+
ignored: [],
|
|
579
|
+
rejected: [],
|
|
580
|
+
warnings,
|
|
581
|
+
});
|
|
582
|
+
try {
|
|
583
|
+
const docResolution = resolveOrgProfileDocument(cwd, opts);
|
|
584
|
+
// Steps 1-3 absent (no pointer, non-string pointer, blank pointer, or
|
|
585
|
+
// package not found): carry the document resolution's `configured` and
|
|
586
|
+
// `warnings` through verbatim into the absent-shaped OrgProfileResolution.
|
|
587
|
+
if (!docResolution.resolved) {
|
|
588
|
+
return {
|
|
589
|
+
...absentShape(docResolution.warnings),
|
|
590
|
+
configured: docResolution.configured,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const warnings = [...docResolution.warnings];
|
|
594
|
+
const doc = docResolution.document;
|
|
595
|
+
// Step 4: flatten and partition by the allowlist (D-06).
|
|
596
|
+
const { leaves, cappedWarning } = flattenProfileDocument(doc);
|
|
597
|
+
if (cappedWarning)
|
|
598
|
+
warnings.push(bound(cappedWarning));
|
|
599
|
+
const contributes = {};
|
|
600
|
+
const ignored = [];
|
|
601
|
+
const rejected = [];
|
|
602
|
+
let ignoredWarningCount = 0;
|
|
603
|
+
let ignoredOverflow = 0;
|
|
604
|
+
// Step 5: validate each allowlisted leaf (D-10); everything else is
|
|
605
|
+
// dropped with a capped warning list (D-06) — except the seed namespace
|
|
606
|
+
// (D-01), which is a KNOWN non-runtime namespace with its own composer
|
|
607
|
+
// (Phase 34), not an unknown key. Skipping it here means it never
|
|
608
|
+
// consumes the PROFILE_IGNORED_WARNING_CAP budget and never makes a
|
|
609
|
+
// seeded profile's `resolveProvider` / `vault status` calls noisier —
|
|
610
|
+
// and it changes no membership in ORG_PROFILE_ALLOWLIST.
|
|
611
|
+
for (const leaf of leaves) {
|
|
612
|
+
if (leaf.key === exports.ORG_PROFILE_SEED_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_SEED_SECTION}.`)) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (!exports.ORG_PROFILE_ALLOWLIST.includes(leaf.key)) {
|
|
616
|
+
ignored.push(leaf.key);
|
|
617
|
+
if (ignoredWarningCount < PROFILE_IGNORED_WARNING_CAP) {
|
|
618
|
+
warnings.push(bound(`org profile key "${sanitizeForWarning(leaf.key)}" is not on the allowlist — ignored.`));
|
|
619
|
+
ignoredWarningCount += 1;
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
ignoredOverflow += 1;
|
|
623
|
+
}
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const disposition = profileValueDisposition(leaf.key, leaf.value);
|
|
627
|
+
if (disposition.ok) {
|
|
628
|
+
contributes[leaf.key] = disposition.value;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
rejected.push(leaf.key);
|
|
632
|
+
warnings.push(bound(`org profile key "${leaf.key}" rejected — ${disposition.reason}.`));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (ignoredOverflow > 0) {
|
|
636
|
+
warnings.push(bound(`${ignoredOverflow} further org profile key(s) were ignored (not on the allowlist).`));
|
|
637
|
+
}
|
|
638
|
+
// Step 6: return. Nothing above throws for any input, but the whole
|
|
639
|
+
// body is still guarded below (D-09) so a fault in a helper this
|
|
640
|
+
// function forgot to wrap still yields the absent shape, not a crash.
|
|
641
|
+
return {
|
|
642
|
+
configured: docResolution.configured,
|
|
643
|
+
resolved: true,
|
|
644
|
+
packageName: docResolution.packageName,
|
|
645
|
+
version: docResolution.version,
|
|
646
|
+
source: docResolution.source,
|
|
647
|
+
dataPath: docResolution.dataPath,
|
|
648
|
+
contributes,
|
|
649
|
+
ignored,
|
|
650
|
+
rejected,
|
|
651
|
+
warnings,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return absentShape(['org profile resolution failed unexpectedly — falling back to built-in defaults.']);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// ─── readProfileValue (D-07) ──────────────────────────────────────────────────
|
|
659
|
+
/**
|
|
660
|
+
* D-07's key-keyed profile layer. This function REPLACES the tier-keyed
|
|
661
|
+
* `orgProfileCoordinates(cwd, tier)` stub Phase 31 froze in
|
|
662
|
+
* `src/vault-coordinates.cts` — a deliberate supersession, not an
|
|
663
|
+
* extension, recorded here and in `src/vault-coordinates.cts`'s own doc
|
|
664
|
+
* comment. D-06's allowlist is a set of config KEYS, not tiers, so one
|
|
665
|
+
* key-keyed function serves all three consuming resolvers
|
|
666
|
+
* (`resolveSharedCoordinates`, `resolvePlatformHost`, and
|
|
667
|
+
* `resolveProvider`), where `provider` and `platform.host` do not fit a
|
|
668
|
+
* per-tier return shape at all. `resolveClientCoordinates` needs no direct
|
|
669
|
+
* call: its host chain reaches this function transitively through
|
|
670
|
+
* `resolvePlatformHost`.
|
|
671
|
+
*
|
|
672
|
+
* When `key` is not in `ORG_PROFILE_ALLOWLIST`, returns
|
|
673
|
+
* `{ value: undefined, warnings: [], source: 'none' }` immediately with NO
|
|
674
|
+
* filesystem access — a caller asking for a non-allowlisted key is a
|
|
675
|
+
* programming error in this module's own consumers, not an untrusted-input
|
|
676
|
+
* case. Otherwise resolves the profile via `resolveOrgProfile(cwd, opts)`
|
|
677
|
+
* and returns that key's `contributes` entry (or `undefined`), the
|
|
678
|
+
* resolution's `warnings`, and its `source`.
|
|
679
|
+
*
|
|
680
|
+
* The profile is re-read from disk on every call, matching every sibling
|
|
681
|
+
* resolver in this seam (`cfgGet`, `readBindings`, `resolveProvider`) —
|
|
682
|
+
* this module introduces no caching. Consumers that call this more than
|
|
683
|
+
* once per resolution MUST de-duplicate the returned warnings by exact
|
|
684
|
+
* string before folding them into their own warning list.
|
|
685
|
+
*/
|
|
686
|
+
function readProfileValue(cwd, key, opts) {
|
|
687
|
+
if (!exports.ORG_PROFILE_ALLOWLIST.includes(key)) {
|
|
688
|
+
return { value: undefined, warnings: [], source: 'none' };
|
|
689
|
+
}
|
|
690
|
+
const resolution = resolveOrgProfile(cwd, opts);
|
|
691
|
+
return {
|
|
692
|
+
value: resolution.contributes[key],
|
|
693
|
+
warnings: resolution.warnings,
|
|
694
|
+
source: resolution.source,
|
|
695
|
+
};
|
|
696
|
+
}
|