@f5-sales-demo/pi-utils 19.51.2
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/package.json +61 -0
- package/src/abortable.ts +86 -0
- package/src/async.ts +50 -0
- package/src/cli.ts +432 -0
- package/src/color.ts +204 -0
- package/src/dirs.ts +480 -0
- package/src/env.ts +108 -0
- package/src/format.ts +106 -0
- package/src/frontmatter.ts +118 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/hook-fetch.ts +30 -0
- package/src/i18n.test.ts +213 -0
- package/src/i18n.ts +90 -0
- package/src/index.ts +63 -0
- package/src/json.ts +10 -0
- package/src/logger.ts +204 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mermaid-color.ts +231 -0
- package/src/mime.ts +159 -0
- package/src/models-yml.ts +162 -0
- package/src/peek-file.ts +114 -0
- package/src/postmortem.ts +197 -0
- package/src/procmgr.ts +326 -0
- package/src/prompt.ts +412 -0
- package/src/ptree.ts +426 -0
- package/src/ring.ts +169 -0
- package/src/snowflake.ts +136 -0
- package/src/stream.ts +394 -0
- package/src/tab-spacing.ts +312 -0
- package/src/temp.ts +77 -0
- package/src/type-guards.ts +11 -0
- package/src/which.ts +232 -0
- package/src/xcsh-context-paths.ts +23 -0
- package/src/xcsh-context-resolver.ts +333 -0
- package/src/xcsh-env-names.ts +69 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface XCSHContextData {
|
|
6
|
+
name: string;
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
apiToken: string;
|
|
9
|
+
defaultNamespace: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
sensitiveKeys?: string[];
|
|
12
|
+
knowledgeSources?: KnowledgeSource[];
|
|
13
|
+
includeSkills?: string[];
|
|
14
|
+
excludeSkills?: string[];
|
|
15
|
+
version?: number;
|
|
16
|
+
metadata?: {
|
|
17
|
+
createdAt?: string;
|
|
18
|
+
expiresAt?: string;
|
|
19
|
+
lastRotatedAt?: string;
|
|
20
|
+
rotateAfterDays?: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface KnowledgeSource {
|
|
25
|
+
url: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
type?: "llms-txt" | "skill-dir" | "docs-site";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ContextOverrides {
|
|
31
|
+
defaultNamespace?: string;
|
|
32
|
+
env?: Record<string, string>;
|
|
33
|
+
sensitiveKeys?: string[];
|
|
34
|
+
knowledgeSources?: KnowledgeSource[];
|
|
35
|
+
includeSkills?: string[];
|
|
36
|
+
excludeSkills?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PointerContext {
|
|
40
|
+
context: string;
|
|
41
|
+
overrides?: ContextOverrides;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ContextSource = "env" | "local" | "global";
|
|
45
|
+
|
|
46
|
+
export interface ResolvedContext {
|
|
47
|
+
context: XCSHContextData;
|
|
48
|
+
source: ContextSource;
|
|
49
|
+
sourcePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Host-supplied path helpers. xcsh injects the `dirs.ts` family; the VS Code
|
|
54
|
+
* extension injects its `contextPaths.ts` family. Keeping the resolver free of a
|
|
55
|
+
* direct `dirs.ts` import keeps this module runtime-agnostic (no Bun globals, no
|
|
56
|
+
* JSON imports) so it bundles cleanly into the VS Code extension via webpack.
|
|
57
|
+
*/
|
|
58
|
+
export interface ContextPathProvider {
|
|
59
|
+
getContextsDir(): string;
|
|
60
|
+
getActiveContextPath(): string;
|
|
61
|
+
getContextPath(name: string): string;
|
|
62
|
+
getLocalContextsDir(cwd: string): string;
|
|
63
|
+
getLocalActiveContextPath(cwd: string): string;
|
|
64
|
+
getLocalContextPath(name: string, cwd: string): string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type GitTracker = (filePath: string) => Promise<boolean>;
|
|
68
|
+
|
|
69
|
+
export interface ResolverDeps {
|
|
70
|
+
paths: ContextPathProvider;
|
|
71
|
+
/** Optional override; defaults to a `node:child_process` implementation that runs under both Bun and Node. */
|
|
72
|
+
gitTracker?: GitTracker;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isPointerContext(data: unknown): data is PointerContext {
|
|
76
|
+
if (data === null || data === undefined || typeof data !== "object") return false;
|
|
77
|
+
const obj = data as Record<string, unknown>;
|
|
78
|
+
return typeof obj.context === "string" && !("apiUrl" in obj);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isInlineContext(data: unknown): boolean {
|
|
82
|
+
if (data === null || data === undefined || typeof data !== "object") return false;
|
|
83
|
+
const obj = data as Record<string, unknown>;
|
|
84
|
+
return typeof obj.apiUrl === "string";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateLocalContextFile(data: unknown): { valid: boolean; error?: string } {
|
|
88
|
+
if (data === null || data === undefined || typeof data !== "object") {
|
|
89
|
+
return { valid: false, error: "Context file must be a JSON object" };
|
|
90
|
+
}
|
|
91
|
+
const obj = data as Record<string, unknown>;
|
|
92
|
+
const hasContext = typeof obj.context === "string";
|
|
93
|
+
const hasApiUrl = typeof obj.apiUrl === "string";
|
|
94
|
+
|
|
95
|
+
if (hasContext && hasApiUrl) {
|
|
96
|
+
return { valid: false, error: "Context file cannot have both 'context' (pointer) and 'apiUrl' (inline) fields" };
|
|
97
|
+
}
|
|
98
|
+
if (!hasContext && !hasApiUrl) {
|
|
99
|
+
return { valid: false, error: "Context file must have either 'context' (pointer) or 'apiUrl' (inline) field" };
|
|
100
|
+
}
|
|
101
|
+
return { valid: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function mergePointerOverrides(base: XCSHContextData, overrides: ContextOverrides): XCSHContextData {
|
|
105
|
+
const merged = { ...base };
|
|
106
|
+
|
|
107
|
+
if (overrides.defaultNamespace !== undefined) {
|
|
108
|
+
merged.defaultNamespace = overrides.defaultNamespace;
|
|
109
|
+
}
|
|
110
|
+
if (overrides.sensitiveKeys !== undefined) {
|
|
111
|
+
merged.sensitiveKeys = overrides.sensitiveKeys;
|
|
112
|
+
}
|
|
113
|
+
if (overrides.knowledgeSources !== undefined) {
|
|
114
|
+
merged.knowledgeSources = overrides.knowledgeSources;
|
|
115
|
+
}
|
|
116
|
+
if (overrides.includeSkills !== undefined) {
|
|
117
|
+
merged.includeSkills = overrides.includeSkills;
|
|
118
|
+
}
|
|
119
|
+
if (overrides.excludeSkills !== undefined) {
|
|
120
|
+
merged.excludeSkills = overrides.excludeSkills;
|
|
121
|
+
}
|
|
122
|
+
if (overrides.env !== undefined) {
|
|
123
|
+
merged.env = { ...base.env, ...overrides.env };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Subcommand names that must not be usable as context names — otherwise
|
|
131
|
+
* `/context <name>` could not disambiguate a switch from a subcommand. Single
|
|
132
|
+
* source of truth shared by both the resolver and the coding agent.
|
|
133
|
+
*/
|
|
134
|
+
export const RESERVED_CONTEXT_NAMES = new Set([
|
|
135
|
+
"list",
|
|
136
|
+
"show",
|
|
137
|
+
"status",
|
|
138
|
+
"create",
|
|
139
|
+
"delete",
|
|
140
|
+
"rename",
|
|
141
|
+
"namespace",
|
|
142
|
+
"env",
|
|
143
|
+
"set",
|
|
144
|
+
"unset",
|
|
145
|
+
"add",
|
|
146
|
+
"remove",
|
|
147
|
+
"clear",
|
|
148
|
+
"activate",
|
|
149
|
+
"validate",
|
|
150
|
+
"export",
|
|
151
|
+
"import",
|
|
152
|
+
"wizard",
|
|
153
|
+
"help",
|
|
154
|
+
"link",
|
|
155
|
+
"unlink",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const CONTEXT_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
159
|
+
|
|
160
|
+
export function isSafeContextName(name: string): boolean {
|
|
161
|
+
if (!CONTEXT_NAME_RE.test(name)) return false;
|
|
162
|
+
return !RESERVED_CONTEXT_NAMES.has(name.toLowerCase());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Normalize an API URL to its origin (`https://host[:port]`) — the canonical
|
|
167
|
+
* stored form for a context endpoint, shared verbatim by the xcsh shell and the
|
|
168
|
+
* VS Code extension.
|
|
169
|
+
*
|
|
170
|
+
* The stored value must be the bare origin only: no path, query, fragment, or
|
|
171
|
+
* trailing slash. Callers append `/api/...` themselves, so the endpoint stays a
|
|
172
|
+
* single consistent value. This also defuses the protocol-relative host
|
|
173
|
+
* collapse: the shared resource library joins URLs by raw concatenation
|
|
174
|
+
* (`${apiUrl}${path}`), and a leftover path or trailing slash would produce a
|
|
175
|
+
* `//` that `new URL()` parses as an authority — collapsing the host to a bare
|
|
176
|
+
* label and breaking TLS altname verification.
|
|
177
|
+
*/
|
|
178
|
+
export function normalizeApiUrl(apiUrl: string): string {
|
|
179
|
+
if (typeof apiUrl !== "string") return apiUrl;
|
|
180
|
+
const trimmed = apiUrl.trim();
|
|
181
|
+
try {
|
|
182
|
+
return new URL(trimmed).origin;
|
|
183
|
+
} catch {
|
|
184
|
+
// Not a parseable absolute URL (input validation should prevent this);
|
|
185
|
+
// fall back to stripping trailing slashes so we never worsen a bad value.
|
|
186
|
+
return trimmed.replace(/\/+$/, "");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function defaultGitTracker(filePath: string): Promise<boolean> {
|
|
191
|
+
try {
|
|
192
|
+
const dir = path.dirname(filePath);
|
|
193
|
+
const res = spawnSync("git", ["ls-files", "--error-unmatch", filePath], {
|
|
194
|
+
cwd: dir,
|
|
195
|
+
stdio: "ignore",
|
|
196
|
+
});
|
|
197
|
+
return Promise.resolve(res.status === 0);
|
|
198
|
+
} catch {
|
|
199
|
+
return Promise.resolve(false);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export class ContextResolver {
|
|
204
|
+
readonly #paths: ContextPathProvider;
|
|
205
|
+
readonly #gitTracker: GitTracker;
|
|
206
|
+
|
|
207
|
+
constructor(deps: ResolverDeps) {
|
|
208
|
+
this.#paths = deps.paths;
|
|
209
|
+
this.#gitTracker = deps.gitTracker ?? defaultGitTracker;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
resolve(cwd: string): Promise<ResolvedContext | null> {
|
|
213
|
+
// Priority 1: environment variables
|
|
214
|
+
const envUrl = process.env.XCSH_API_URL;
|
|
215
|
+
const envToken = process.env.XCSH_API_TOKEN;
|
|
216
|
+
if (envUrl && envToken) {
|
|
217
|
+
return Promise.resolve(
|
|
218
|
+
this.#finalize(
|
|
219
|
+
{
|
|
220
|
+
name: "(env)",
|
|
221
|
+
apiUrl: envUrl,
|
|
222
|
+
apiToken: envToken,
|
|
223
|
+
defaultNamespace: process.env.XCSH_NAMESPACE ?? "system",
|
|
224
|
+
},
|
|
225
|
+
"env",
|
|
226
|
+
"environment variables",
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Priority 2: local .xcsh/contexts/
|
|
232
|
+
const localDir = this.findLocalContextsDir(cwd);
|
|
233
|
+
if (localDir) {
|
|
234
|
+
const localResult = this.#resolveFromDir("local", cwd);
|
|
235
|
+
if (localResult) return Promise.resolve(localResult);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Priority 3: global ~/.config/xcsh/contexts/
|
|
239
|
+
return Promise.resolve(this.#resolveGlobal());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
findLocalContextsDir(cwd: string): string | null {
|
|
243
|
+
const dir = this.#paths.getLocalContextsDir(cwd);
|
|
244
|
+
return fs.existsSync(dir) ? dir : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
checkGitTracking(filePath: string): Promise<boolean> {
|
|
248
|
+
return this.#gitTracker(filePath);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#resolveFromDir(source: ContextSource, cwd: string): ResolvedContext | null {
|
|
252
|
+
const activeContextPath =
|
|
253
|
+
source === "local" ? this.#paths.getLocalActiveContextPath(cwd) : this.#paths.getActiveContextPath();
|
|
254
|
+
|
|
255
|
+
const activeName = this.#readActivePointer(activeContextPath);
|
|
256
|
+
if (!activeName || !isSafeContextName(activeName)) return null;
|
|
257
|
+
|
|
258
|
+
const contextPath =
|
|
259
|
+
source === "local" ? this.#paths.getLocalContextPath(activeName, cwd) : this.#paths.getContextPath(activeName);
|
|
260
|
+
|
|
261
|
+
const data = this.#readJsonFile(contextPath);
|
|
262
|
+
if (!data) return null;
|
|
263
|
+
|
|
264
|
+
const validation = validateLocalContextFile(data);
|
|
265
|
+
if (!validation.valid) return null;
|
|
266
|
+
|
|
267
|
+
if (isPointerContext(data)) {
|
|
268
|
+
return this.#resolvePointer(data, contextPath);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isInlineContext(data)) {
|
|
272
|
+
const obj = data as Record<string, unknown>;
|
|
273
|
+
if (
|
|
274
|
+
typeof obj.name !== "string" ||
|
|
275
|
+
typeof obj.apiToken !== "string" ||
|
|
276
|
+
typeof obj.defaultNamespace !== "string"
|
|
277
|
+
) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
return this.#finalize(data as unknown as XCSHContextData, source, contextPath);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#resolveGlobal(): ResolvedContext | null {
|
|
287
|
+
const globalContextsDir = this.#paths.getContextsDir();
|
|
288
|
+
if (!fs.existsSync(globalContextsDir)) return null;
|
|
289
|
+
return this.#resolveFromDir("global", "");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#resolvePointer(pointer: PointerContext, pointerPath: string): ResolvedContext | null {
|
|
293
|
+
if (!isSafeContextName(pointer.context)) return null;
|
|
294
|
+
const globalPath = this.#paths.getContextPath(pointer.context);
|
|
295
|
+
const globalData = this.#readJsonFile(globalPath);
|
|
296
|
+
if (!globalData) return null;
|
|
297
|
+
|
|
298
|
+
let resolved = globalData as unknown as XCSHContextData;
|
|
299
|
+
if (pointer.overrides) {
|
|
300
|
+
resolved = mergePointerOverrides(resolved, pointer.overrides);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// A pointer always resolves through the local tier, so report "local".
|
|
304
|
+
return this.#finalize(resolved, "local", pointerPath);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Stamp source/path and normalize the resolved apiUrl uniformly across all sources. */
|
|
308
|
+
#finalize(context: XCSHContextData, source: ContextSource, sourcePath: string): ResolvedContext {
|
|
309
|
+
const normalized =
|
|
310
|
+
typeof context.apiUrl === "string" ? { ...context, apiUrl: normalizeApiUrl(context.apiUrl) } : context;
|
|
311
|
+
return { context: normalized, source, sourcePath };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#readActivePointer(filePath: string): string | null {
|
|
315
|
+
if (!fs.existsSync(filePath)) return null;
|
|
316
|
+
try {
|
|
317
|
+
const name = fs.readFileSync(filePath, "utf-8").trim();
|
|
318
|
+
return name || null;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#readJsonFile(filePath: string): Record<string, unknown> | null {
|
|
325
|
+
if (!fs.existsSync(filePath)) return null;
|
|
326
|
+
try {
|
|
327
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
328
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical F5 XC context environment-variable names and the secret-detection
|
|
3
|
+
* rule, shared by every host (the xcsh shell and the VS Code extension) so both
|
|
4
|
+
* agree on which keys are reserved, which are recognized auth credentials, and
|
|
5
|
+
* which values must be masked.
|
|
6
|
+
*
|
|
7
|
+
* This module is intentionally dependency-free (no Bun/Node APIs) so a CommonJS
|
|
8
|
+
* `require()` from the extension can load it without dragging in the Bun-only
|
|
9
|
+
* barrel. Names are typed `as const` so bracket access like
|
|
10
|
+
* `process.env[XCSH_API_URL]` type-checks correctly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const XCSH_API_URL = "XCSH_API_URL" as const;
|
|
14
|
+
export const XCSH_API_TOKEN = "XCSH_API_TOKEN" as const;
|
|
15
|
+
export const XCSH_NAMESPACE = "XCSH_NAMESPACE" as const;
|
|
16
|
+
export const XCSH_TENANT = "XCSH_TENANT" as const;
|
|
17
|
+
export const XCSH_USERNAME = "XCSH_USERNAME" as const;
|
|
18
|
+
export const XCSH_CONSOLE_PASSWORD = "XCSH_CONSOLE_PASSWORD" as const;
|
|
19
|
+
/** Active context profile name. Read-only metadata injected by ContextService. */
|
|
20
|
+
export const XCSH_CONTEXT_NAME = "XCSH_CONTEXT_NAME" as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Control env vars owned by the context itself (apiUrl/apiToken/defaultNamespace
|
|
24
|
+
* → XCSH_API_URL/XCSH_API_TOKEN/XCSH_NAMESPACE), derived (XCSH_TENANT), or
|
|
25
|
+
* injected at activation (XCSH_CONTEXT_NAME). A context's custom `env` map must
|
|
26
|
+
* never set these — they would be ignored or clobbered by the resolver.
|
|
27
|
+
*/
|
|
28
|
+
export const RESERVED_ENV_KEYS: ReadonlySet<string> = new Set([
|
|
29
|
+
XCSH_NAMESPACE,
|
|
30
|
+
XCSH_API_URL,
|
|
31
|
+
XCSH_API_TOKEN,
|
|
32
|
+
XCSH_TENANT,
|
|
33
|
+
XCSH_CONTEXT_NAME,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Recognized web-console login credentials. Unlike RESERVED_ENV_KEYS these live
|
|
38
|
+
* in the context's generic `env` map (a user sets them like any other variable),
|
|
39
|
+
* but hosts surface them in a dedicated "Auth" section, in display order.
|
|
40
|
+
*/
|
|
41
|
+
export const AUTH_ENV_KEYS: readonly string[] = [XCSH_USERNAME, XCSH_CONSOLE_PASSWORD];
|
|
42
|
+
|
|
43
|
+
/** Env var name patterns that indicate a secret value (mask in output, redact on export). */
|
|
44
|
+
export const SECRET_ENV_PATTERNS = /(?:KEY|SECRET|TOKEN|PASSWORD|PASS|AUTH|CREDENTIAL|PRIVATE|OAUTH)(?:_|$)/i;
|
|
45
|
+
|
|
46
|
+
/** True iff an env var NAME looks like it holds a secret (e.g. XCSH_CONSOLE_PASSWORD). */
|
|
47
|
+
export function isSensitiveEnvKey(key: string): boolean {
|
|
48
|
+
return SECRET_ENV_PATTERNS.test(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Prefix that namespaces every context-owned environment variable. */
|
|
52
|
+
export const XCSH_ENV_PREFIX = "XCSH_";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* True iff a context's `env` entry may be injected into a spawned subprocess.
|
|
56
|
+
*
|
|
57
|
+
* This is an allowlist (default-deny): only `XCSH_`-namespaced, non-reserved keys
|
|
58
|
+
* are injectable. A project-local `.xcsh/contexts/*.json` is untrusted input, so a
|
|
59
|
+
* denylist of "dangerous" names is unsafe — it is impossible to enumerate every
|
|
60
|
+
* process/interpreter-hijacking variable (LD_PRELOAD, DYLD_INSERT_LIBRARIES,
|
|
61
|
+
* NODE_OPTIONS, NODE_PATH, PATH, PYTHONHOME, JAVA_TOOL_OPTIONS, CLASSPATH, …).
|
|
62
|
+
* Restricting injection to the `XCSH_` namespace blocks all of them by
|
|
63
|
+
* construction, since none are `XCSH_`-prefixed. Reserved keys (XCSH_API_URL,
|
|
64
|
+
* XCSH_API_TOKEN, XCSH_NAMESPACE, XCSH_TENANT, XCSH_CONTEXT_NAME) are excluded too —
|
|
65
|
+
* the host sets those itself from the context's typed fields.
|
|
66
|
+
*/
|
|
67
|
+
export function isInjectableContextEnvKey(key: string): boolean {
|
|
68
|
+
return key.startsWith(XCSH_ENV_PREFIX) && !RESERVED_ENV_KEYS.has(key);
|
|
69
|
+
}
|