@procedures/env 0.0.0-stage → 0.2.3

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +7 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/module.d.ts +45 -0
  9. package/dist/module.d.ts.map +1 -0
  10. package/dist/module.js +143 -0
  11. package/dist/module.js.map +1 -0
  12. package/dist/procedures/env/check.d.ts +12 -0
  13. package/dist/procedures/env/check.d.ts.map +1 -0
  14. package/dist/procedures/env/check.js +21 -0
  15. package/dist/procedures/env/check.js.map +1 -0
  16. package/dist/procedures/env/get.d.ts +13 -0
  17. package/dist/procedures/env/get.d.ts.map +1 -0
  18. package/dist/procedures/env/get.js +24 -0
  19. package/dist/procedures/env/get.js.map +1 -0
  20. package/dist/procedures/env/index.d.ts +4 -0
  21. package/dist/procedures/env/index.d.ts.map +1 -0
  22. package/dist/procedures/env/index.js +4 -0
  23. package/dist/procedures/env/index.js.map +1 -0
  24. package/dist/procedures/env/list.d.ts +14 -0
  25. package/dist/procedures/env/list.d.ts.map +1 -0
  26. package/dist/procedures/env/list.js +27 -0
  27. package/dist/procedures/env/list.js.map +1 -0
  28. package/dist/register.d.ts +6 -0
  29. package/dist/register.d.ts.map +1 -0
  30. package/dist/register.js +8 -0
  31. package/dist/register.js.map +1 -0
  32. package/dist/secrets.d.ts +28 -0
  33. package/dist/secrets.d.ts.map +1 -0
  34. package/dist/secrets.js +40 -0
  35. package/dist/secrets.js.map +1 -0
  36. package/dist/types.d.ts +57 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +8 -0
  39. package/dist/types.js.map +1 -0
  40. package/package.json +75 -3
  41. package/src/index.ts +6 -0
  42. package/src/module.ts +186 -0
  43. package/src/procedures/env/check.ts +23 -0
  44. package/src/procedures/env/get.ts +26 -0
  45. package/src/procedures/env/index.ts +3 -0
  46. package/src/procedures/env/list.ts +27 -0
  47. package/src/register.ts +10 -0
  48. package/src/secrets.ts +56 -0
  49. package/src/types.ts +76 -0
package/src/module.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The procedures of @procedures/env as data. Importing this file registers nothing.
3
+ *
4
+ * Scaffold a procedure with: mark procedure new <name> --path packages/env
5
+ *
6
+ * `envModule` reads `process.env` and masks each secret. `createEnvModule()` gives the same
7
+ * procedures with more secret patterns, secrets that `env.get` reveals, or other variables
8
+ * (roadmap R46). These procedures are not MCP tools: a value goes to the model and its provider.
9
+ */
10
+
11
+ import { z } from "zod";
12
+ import {
13
+ defineModule,
14
+ registerModules,
15
+ PROCEDURE_REGISTRY,
16
+ createProcedure,
17
+ zodAdapter,
18
+ type AnyProcedure,
19
+ type Effects,
20
+ type ProcedureModule,
21
+ type ProcedureRegistry,
22
+ } from "@procedures/client";
23
+ import { envGet } from "./procedures/env/get.js";
24
+ import { envList } from "./procedures/env/list.js";
25
+ import { envCheck } from "./procedures/env/check.js";
26
+ import { DEFAULT_ENV_SETTINGS, DEFAULT_SECRET_PATTERNS, secretMatcher, type EnvSettings } from "./secrets.js";
27
+ import type { EnvCheckInput, EnvCheckOutput, EnvGetInput, EnvGetOutput, EnvListInput, EnvListOutput, EnvSource } from "./types.js";
28
+
29
+ // =============================================================================
30
+ // Schemas
31
+ // =============================================================================
32
+
33
+ /** The name of a variable: not empty, without `=` and without a NUL character. */
34
+ const variableName = z
35
+ .string()
36
+ .min(1)
37
+ .max(1024)
38
+ .regex(/^[^=\0]+$/, "A name without = and without NUL")
39
+ .describe("The name of an environment variable");
40
+
41
+ const EnvGetInputSchema = z.object({ name: variableName });
42
+
43
+ const EnvGetOutputSchema = z.object({
44
+ name: z.string().describe("The name of the variable"),
45
+ set: z.boolean().describe("True when the variable has a value. An empty value counts as set"),
46
+ secret: z.boolean().describe("True when the name matches a pattern of secret names"),
47
+ masked: z.boolean().describe("True for a secret that the host does not reveal: the output has no value"),
48
+ value: z.string().optional().describe("The value. Absent for a variable that is not set and for a masked secret"),
49
+ });
50
+
51
+ const EnvListInputSchema = z.object({
52
+ prefix: z.string().max(1024).optional().describe("Only the names that start with this text. The comparison ignores the case"),
53
+ values: z.boolean().optional().describe("True: give the value of each variable that is not a secret (default false)"),
54
+ });
55
+
56
+ const EnvListOutputSchema = z.object({
57
+ variables: z
58
+ .array(
59
+ z.object({
60
+ name: z.string(),
61
+ secret: z.boolean().describe("True when the name matches a pattern of secret names"),
62
+ value: z.string().optional().describe("The value: only with values: true, and never for a secret"),
63
+ }),
64
+ )
65
+ .describe("The variables, sorted by name"),
66
+ });
67
+
68
+ const EnvCheckInputSchema = z.object({
69
+ required: z.array(variableName).max(1000).describe("The names of the variables that must have a value"),
70
+ allowEmpty: z.boolean().optional().describe("True: an empty value counts as present (default false)"),
71
+ });
72
+
73
+ const EnvCheckOutputSchema = z.object({
74
+ ok: z.boolean().describe("True when no required variable is missing"),
75
+ missing: z.array(z.string()).describe("The required variables without a value"),
76
+ present: z.array(z.string()).describe("The required variables with a value"),
77
+ });
78
+
79
+ // =============================================================================
80
+ // Procedures
81
+ // =============================================================================
82
+
83
+ /** The effects of each procedure: it reads the environment of the process and changes nothing. */
84
+ const READS_PROCESS: Effects = Object.freeze({ reads: Object.freeze(["process"]), writes: Object.freeze([]) });
85
+
86
+ /** The three procedures, with handlers that use the settings. */
87
+ function envProcedures(settings: EnvSettings): AnyProcedure[] {
88
+ const envGetProcedure = createProcedure()
89
+ .path(["env", "get"])
90
+ .input(zodAdapter<EnvGetInput>(EnvGetInputSchema))
91
+ .output(zodAdapter<EnvGetOutput>(EnvGetOutputSchema))
92
+ .meta({
93
+ description: "One environment variable. A secret is masked unless the host reveals it",
94
+ tags: ["env", "secrets"],
95
+ args: ["name"],
96
+ shorts: {},
97
+ output: "json",
98
+ effects: READS_PROCESS,
99
+ })
100
+ .handler(async (input: EnvGetInput): Promise<EnvGetOutput> => envGet(input, settings))
101
+ .build();
102
+
103
+ const envListProcedure = createProcedure()
104
+ .path(["env", "list"])
105
+ .input(zodAdapter<EnvListInput>(EnvListInputSchema))
106
+ .output(zodAdapter<EnvListOutput>(EnvListOutputSchema))
107
+ .meta({
108
+ description: "The names of the environment variables, never the values of the secrets",
109
+ tags: ["env", "secrets"],
110
+ args: ["prefix"],
111
+ shorts: {},
112
+ output: "json",
113
+ effects: READS_PROCESS,
114
+ })
115
+ .handler(async (input: EnvListInput): Promise<EnvListOutput> => envList(input, settings))
116
+ .build();
117
+
118
+ const envCheckProcedure = createProcedure()
119
+ .path(["env", "check"])
120
+ .input(zodAdapter<EnvCheckInput>(EnvCheckInputSchema))
121
+ .output(zodAdapter<EnvCheckOutput>(EnvCheckOutputSchema))
122
+ .meta({
123
+ description: "Which required environment variables are missing",
124
+ tags: ["env"],
125
+ args: ["required"],
126
+ shorts: {},
127
+ output: "json",
128
+ effects: READS_PROCESS,
129
+ })
130
+ .handler(async (input: EnvCheckInput): Promise<EnvCheckOutput> => envCheck(input, settings))
131
+ .build();
132
+
133
+ return [envGetProcedure, envListProcedure, envCheckProcedure];
134
+ }
135
+
136
+ /**
137
+ * The module of this package, for `createRegistry()`: its procedures as data. It reads
138
+ * `process.env` at each call and masks each secret. Importing this file registers nothing.
139
+ */
140
+ export const envModule: ProcedureModule = defineModule("@procedures/env", envProcedures(DEFAULT_ENV_SETTINGS));
141
+
142
+ export default envModule;
143
+
144
+ /** The options of `createEnvModule()`. */
145
+ export interface EnvModuleOptions {
146
+ /** More patterns of secret names, after the default patterns. `*` matches each sequence of characters. */
147
+ readonly secretPatterns?: readonly string[] | undefined;
148
+ /**
149
+ * The secrets whose value `env.get` gives: `true` for each secret, or a list of names (the
150
+ * comparison ignores the case). The default is none. `env.list` never gives a secret.
151
+ */
152
+ readonly revealSecrets?: boolean | readonly string[] | undefined;
153
+ /** The variables: a record, or a function that gives them at each call. The default is `process.env`. */
154
+ readonly env?: EnvSource | (() => EnvSource) | undefined;
155
+ }
156
+
157
+ /**
158
+ * This function gives the module of this package with other settings. Without options, it gives
159
+ * `envModule`. A host gives it to `createRegistry()` before its bundles, so it replaces the
160
+ * `envModule` that a bundle requires (the first module of a name counts).
161
+ *
162
+ * ```ts
163
+ * createRegistry(createEnvModule({ secretPatterns: ["DATABASE_URL"], revealSecrets: ["NPM_TOKEN"] }));
164
+ * ```
165
+ */
166
+ export function createEnvModule(options: EnvModuleOptions = {}): ProcedureModule {
167
+ if (Object.values(options).every((value) => value === undefined)) return envModule;
168
+ const env = options.env;
169
+ const source = env === undefined ? DEFAULT_ENV_SETTINGS.source : typeof env === "function" ? env : () => env;
170
+ const reveal = options.revealSecrets;
171
+ const revealed = Array.isArray(reveal) ? new Set(reveal.map((name) => name.toUpperCase())) : undefined;
172
+ const settings: EnvSettings = {
173
+ source,
174
+ isSecret: secretMatcher([...DEFAULT_SECRET_PATTERNS, ...(options.secretPatterns ?? [])]),
175
+ reveals: reveal === true ? () => true : revealed ? (name) => revealed.has(name.toUpperCase()) : () => false,
176
+ };
177
+ return defineModule("@procedures/env", envProcedures(settings));
178
+ }
179
+
180
+ /**
181
+ * This function registers the procedures of @procedures/env in a registry. The default is the
182
+ * global registry. The options are the options of `createEnvModule()`.
183
+ */
184
+ export function registerEnvProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY, options: EnvModuleOptions = {}): void {
185
+ registerModules(registry, [createEnvModule(options)]);
186
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * env.check procedure
3
+ *
4
+ * Which required environment variables are missing. The output has names only, never values.
5
+ */
6
+
7
+ import { DEFAULT_ENV_SETTINGS, valueOf, type EnvSettings } from "../../secrets.js";
8
+ import type { EnvCheckInput, EnvCheckOutput } from "../../types.js";
9
+
10
+ /**
11
+ * Which required environment variables are missing
12
+ */
13
+ export async function envCheck(input: EnvCheckInput, settings: EnvSettings = DEFAULT_ENV_SETTINGS): Promise<EnvCheckOutput> {
14
+ const source = settings.source();
15
+ const missing: string[] = [];
16
+ const present: string[] = [];
17
+ for (const name of new Set(input.required)) {
18
+ const value = valueOf(source, name);
19
+ const has = value !== undefined && (input.allowEmpty === true || value !== "");
20
+ (has ? present : missing).push(name);
21
+ }
22
+ return { ok: missing.length === 0, missing, present };
23
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * env.get procedure
3
+ *
4
+ * One environment variable. A secret is masked unless the host reveals it: the output then says
5
+ * that the variable is set, and it has no value.
6
+ */
7
+
8
+ import { DEFAULT_ENV_SETTINGS, valueOf, type EnvSettings } from "../../secrets.js";
9
+ import type { EnvGetInput, EnvGetOutput } from "../../types.js";
10
+
11
+ /**
12
+ * One environment variable. A secret is masked unless the host reveals it
13
+ */
14
+ export async function envGet(input: EnvGetInput, settings: EnvSettings = DEFAULT_ENV_SETTINGS): Promise<EnvGetOutput> {
15
+ const value = valueOf(settings.source(), input.name);
16
+ const secret = settings.isSecret(input.name);
17
+ const set = value !== undefined;
18
+ const masked = set && secret && !settings.reveals(input.name);
19
+ return {
20
+ name: input.name,
21
+ set,
22
+ secret,
23
+ masked,
24
+ ...(set && !masked ? { value } : {}),
25
+ };
26
+ }
@@ -0,0 +1,3 @@
1
+ export { envGet } from "./get.js";
2
+ export { envList } from "./list.js";
3
+ export { envCheck } from "./check.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * env.list procedure
3
+ *
4
+ * The names of the environment variables, never the values of the secrets. With `values: true`,
5
+ * the output has the values of the other variables. The reveal rule of the host does not apply
6
+ * here: a list never shows a secret.
7
+ */
8
+
9
+ import { DEFAULT_ENV_SETTINGS, type EnvSettings } from "../../secrets.js";
10
+ import type { EnvListEntry, EnvListInput, EnvListOutput } from "../../types.js";
11
+
12
+ /**
13
+ * The names of the environment variables, never the values of the secrets
14
+ */
15
+ export async function envList(input: EnvListInput, settings: EnvSettings = DEFAULT_ENV_SETTINGS): Promise<EnvListOutput> {
16
+ const source = settings.source();
17
+ const prefix = input.prefix?.toUpperCase();
18
+ const variables: EnvListEntry[] = [];
19
+ for (const [name, value] of Object.entries(source)) {
20
+ if (typeof value !== "string") continue;
21
+ if (prefix !== undefined && !name.toUpperCase().startsWith(prefix)) continue;
22
+ const secret = settings.isSecret(name);
23
+ variables.push({ name, secret, ...(input.values === true && !secret ? { value } : {}) });
24
+ }
25
+ variables.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
26
+ return { variables };
27
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/env in the global registry:
3
+ * `import "@procedures/env/register"`. The package root registers nothing.
4
+ */
5
+
6
+ import { registerEnvProcedures } from "./module.js";
7
+
8
+ export * from "./module.js";
9
+
10
+ registerEnvProcedures();
package/src/secrets.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The secret names and the settings of the env procedures.
3
+ *
4
+ * A secret is a variable whose name matches a pattern. A pattern is a name with `*` for each
5
+ * sequence of characters (also an empty one). The comparison ignores the case, because Windows
6
+ * ignores the case of the names of variables.
7
+ */
8
+
9
+ import type { EnvSource } from "./types.js";
10
+
11
+ /** The default patterns of secret names. */
12
+ export const DEFAULT_SECRET_PATTERNS: readonly string[] = Object.freeze(["*_TOKEN", "*_KEY", "*_SECRET", "*PASSWORD*"]);
13
+
14
+ /** This function gives the regular expression of a pattern: `*` matches each sequence of characters. */
15
+ function patternExpression(pattern: string): RegExp {
16
+ const escaped = pattern
17
+ .split("*")
18
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
19
+ .join(".*");
20
+ return new RegExp(`^${escaped}$`, "i");
21
+ }
22
+
23
+ /** This function gives the test of the secret names for a list of patterns. */
24
+ export function secretMatcher(patterns: readonly string[]): (name: string) => boolean {
25
+ const expressions = patterns.map((pattern) => {
26
+ if (typeof pattern !== "string" || pattern.trim() === "") throw new TypeError("A secret pattern must be a text that is not empty");
27
+ return patternExpression(pattern);
28
+ });
29
+ return (name) => expressions.some((expression) => expression.test(name));
30
+ }
31
+
32
+ /** True when the name matches one of the default patterns of secret names. */
33
+ export const isDefaultSecret: (name: string) => boolean = secretMatcher(DEFAULT_SECRET_PATTERNS);
34
+
35
+ /** What the procedures of a module use: the variables, the secret test and the reveal rule. */
36
+ export interface EnvSettings {
37
+ /** This function gives the variables at the time of a call. */
38
+ readonly source: () => EnvSource;
39
+ /** True when a name is a secret. */
40
+ readonly isSecret: (name: string) => boolean;
41
+ /** True when `env.get` gives the value of this secret. */
42
+ readonly reveals: (name: string) => boolean;
43
+ }
44
+
45
+ /** The settings of the default module: `process.env`, the default patterns, and no secret revealed. */
46
+ export const DEFAULT_ENV_SETTINGS: EnvSettings = Object.freeze({
47
+ source: () => process.env,
48
+ isSecret: isDefaultSecret,
49
+ reveals: () => false,
50
+ });
51
+
52
+ /** The value of a variable, or undefined. The lookup is the lookup of the source: `process.env` ignores the case on Windows. */
53
+ export function valueOf(source: EnvSource, name: string): string | undefined {
54
+ const value = source[name];
55
+ return typeof value === "string" ? value : undefined;
56
+ }
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Type definitions of the procedures of this package
3
+ *
4
+ * mark procedure new adds the types of each new procedure here. The zod schemas are in
5
+ * `module.ts`: an exported schema needs a written type (isolatedDeclarations).
6
+ */
7
+
8
+ /** The variables that the procedures read: a name and a value, or undefined for a variable that is not set. */
9
+ export type EnvSource = Readonly<Record<string, string | undefined>>;
10
+
11
+ // =============================================================================
12
+ // env.get
13
+ // =============================================================================
14
+
15
+ export interface EnvGetInput {
16
+ /** The name of the variable. */
17
+ name: string;
18
+ }
19
+
20
+ export interface EnvGetOutput {
21
+ /** The name of the variable. */
22
+ name: string;
23
+ /** True when the variable has a value. An empty value counts as set. */
24
+ set: boolean;
25
+ /** True when the name matches a pattern of secret names. */
26
+ secret: boolean;
27
+ /** True when the variable is a secret and the host does not reveal it: the output has no value. */
28
+ masked: boolean;
29
+ /** The value. It is absent for a variable that is not set and for a masked secret. */
30
+ value?: string | undefined;
31
+ }
32
+
33
+ // =============================================================================
34
+ // env.list
35
+ // =============================================================================
36
+
37
+ export interface EnvListInput {
38
+ /** Only the names that start with this text. The comparison ignores the case. */
39
+ prefix?: string | undefined;
40
+ /** True: the output has the value of each variable that is not a secret. The default is false. */
41
+ values?: boolean | undefined;
42
+ }
43
+
44
+ /** One variable of `env.list`. */
45
+ export interface EnvListEntry {
46
+ name: string;
47
+ /** True when the name matches a pattern of secret names. */
48
+ secret: boolean;
49
+ /** The value, only with `values: true` and only for a variable that is not a secret. */
50
+ value?: string | undefined;
51
+ }
52
+
53
+ export interface EnvListOutput {
54
+ /** The variables, sorted by name. */
55
+ variables: EnvListEntry[];
56
+ }
57
+
58
+ // =============================================================================
59
+ // env.check
60
+ // =============================================================================
61
+
62
+ export interface EnvCheckInput {
63
+ /** The names of the variables that must have a value. */
64
+ required: string[];
65
+ /** True: an empty value counts as present. The default is false: an empty value counts as missing. */
66
+ allowEmpty?: boolean | undefined;
67
+ }
68
+
69
+ export interface EnvCheckOutput {
70
+ /** True when no required variable is missing. */
71
+ ok: boolean;
72
+ /** The required variables that are not set (or that are empty), in the order of the input. */
73
+ missing: string[];
74
+ /** The required variables that have a value, in the order of the input. */
75
+ present: string[];
76
+ }