@enactprotocol/secrets 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @enactprotocol/secrets
2
+
3
+ OS keyring integration and environment variable management for Enact.
4
+
5
+ ## Overview
6
+
7
+ This package provides:
8
+ - OS-native keyring storage (macOS Keychain, Windows Credential Manager, Linux Secret Service)
9
+ - Namespace-scoped secret resolution with inheritance
10
+ - `.env` file management (global and local)
11
+ - Dagger secret URI scheme support
12
+ - Secure secret handling with memory-only runtime
13
+
14
+ ## Architecture
15
+
16
+ ### Secret Storage
17
+
18
+ Secrets are stored in the OS keyring with the service name `enact-cli` and account identifier `{namespace}:{SECRET_NAME}`.
19
+
20
+ Examples:
21
+ - `alice/api:API_TOKEN`
22
+ - `acme-corp/data:DATABASE_URL`
23
+
24
+ ### Namespace Inheritance
25
+
26
+ When resolving secrets, Enact walks up the namespace path:
27
+
28
+ ```
29
+ Tool: alice/api/slack/notifier
30
+ Needs: API_TOKEN
31
+
32
+ Lookup:
33
+ 1. alice/api/slack:API_TOKEN
34
+ 2. alice/api:API_TOKEN ✓ found
35
+ 3. alice:API_TOKEN
36
+ ```
37
+
38
+ First match wins.
39
+
40
+ ### Environment Variables
41
+
42
+ Non-secret environment variables are stored in `.env` files with priority:
43
+
44
+ 1. **Local project** (`.enact/.env`) - highest priority
45
+ 2. **Global user** (`~/.enact/.env`)
46
+ 3. **Default values** from tool manifest - lowest priority
47
+
48
+ ## Status
49
+
50
+ Currently in Phase 1 (scaffolding). Full implementation will be completed in Phase 3.
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ # Build
56
+ bun run build
57
+
58
+ # Test
59
+ bun test
60
+
61
+ # Type check
62
+ bun run typecheck
63
+ ```
64
+
65
+ ## Planned Features (Phase 3)
66
+
67
+ - [ ] Keyring integration (@zowe/secrets-for-zowe-sdk)
68
+ - [ ] setSecret() / getSecret() / listSecrets() / deleteSecret()
69
+ - [ ] Namespace inheritance resolution
70
+ - [ ] .env file reading and writing
71
+ - [ ] Dagger secret URI parsing (env://, file://, cmd://, op://, vault://)
72
+ - [ ] Cross-platform testing
73
+ - [ ] Comprehensive test coverage
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@enactprotocol/secrets",
3
+ "version": "2.0.0",
4
+ "description": "OS keyring integration and environment variable management for Enact",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc --build",
16
+ "clean": "rm -rf dist",
17
+ "test": "bun test",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "dependencies": {
21
+ "@zowe/secrets-for-zowe-sdk": "^8.0.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.10.1",
25
+ "typescript": "^5.7.2"
26
+ },
27
+ "keywords": ["keyring", "secrets", "environment", "credentials"],
28
+ "author": "Enact Protocol",
29
+ "license": "Apache-2.0"
30
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Dagger secret integration
3
+ *
4
+ * Re-exports all dagger-related functions
5
+ */
6
+
7
+ export {
8
+ parseSecretUri,
9
+ resolveSecretUri,
10
+ isSecretUri,
11
+ getSupportedSchemes,
12
+ } from "./uri-parser";
13
+
14
+ export {
15
+ getSecretObject,
16
+ getSecretObjects,
17
+ parseSecretOverride,
18
+ parseSecretOverrides,
19
+ } from "./secret-object";
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Get secret objects for Dagger integration
3
+ *
4
+ * Combines keyring secrets with Dagger URI overrides
5
+ */
6
+
7
+ import { resolveSecret } from "../resolver";
8
+ import type { GetSecretOptions, SecretObject } from "../types";
9
+ import { isSecretUri, resolveSecretUri } from "./uri-parser";
10
+
11
+ /**
12
+ * Get a secret object for Dagger
13
+ *
14
+ * If an override URI is provided, resolves that instead of keyring.
15
+ * Otherwise, resolves from keyring with namespace inheritance.
16
+ *
17
+ * @param toolPath - The tool path for namespace resolution
18
+ * @param secretName - The secret name
19
+ * @param options - Options including override URI
20
+ * @returns Secret object with name, value, and source info
21
+ * @throws Error if secret not found
22
+ */
23
+ export async function getSecretObject(
24
+ toolPath: string,
25
+ secretName: string,
26
+ options: GetSecretOptions = {}
27
+ ): Promise<SecretObject> {
28
+ const { overrideUri } = options;
29
+
30
+ // If override URI is provided, resolve it
31
+ if (overrideUri) {
32
+ const value = await resolveSecretUri(overrideUri);
33
+ return {
34
+ name: secretName,
35
+ value,
36
+ source: "override",
37
+ overrideUri,
38
+ };
39
+ }
40
+
41
+ // Otherwise, resolve from keyring
42
+ const result = await resolveSecret(toolPath, secretName);
43
+
44
+ if (!result.found) {
45
+ throw new Error(
46
+ `Secret '${secretName}' not found for tool '${toolPath}'. ` +
47
+ `Searched namespaces: ${result.searchedNamespaces.join(", ")}. ` +
48
+ `Set with: enact env set ${secretName} --secret --namespace <namespace>`
49
+ );
50
+ }
51
+
52
+ return {
53
+ name: secretName,
54
+ value: result.value,
55
+ source: "keyring",
56
+ namespace: result.namespace,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Get multiple secret objects for a tool
62
+ *
63
+ * @param toolPath - The tool path for namespace resolution
64
+ * @param secrets - Map of secret name to optional override URI
65
+ * @returns Map of secret name to secret object
66
+ */
67
+ export async function getSecretObjects(
68
+ toolPath: string,
69
+ secrets: Record<string, string | undefined>
70
+ ): Promise<Map<string, SecretObject>> {
71
+ const results = new Map<string, SecretObject>();
72
+
73
+ for (const [name, overrideUri] of Object.entries(secrets)) {
74
+ const obj = await getSecretObject(toolPath, name, overrideUri ? { overrideUri } : {});
75
+ results.set(name, obj);
76
+ }
77
+
78
+ return results;
79
+ }
80
+
81
+ /**
82
+ * Parse a secret override from CLI format
83
+ *
84
+ * Format: SECRET_NAME=uri
85
+ * Example: API_TOKEN=env://MY_API_TOKEN
86
+ *
87
+ * @param override - The override string
88
+ * @returns Parsed name and URI, or null if invalid
89
+ */
90
+ export function parseSecretOverride(override: string): { name: string; uri: string } | null {
91
+ const eqIndex = override.indexOf("=");
92
+ if (eqIndex === -1) {
93
+ return null;
94
+ }
95
+
96
+ const name = override.slice(0, eqIndex).trim();
97
+ const uri = override.slice(eqIndex + 1).trim();
98
+
99
+ if (!name || !isSecretUri(uri)) {
100
+ return null;
101
+ }
102
+
103
+ return { name, uri };
104
+ }
105
+
106
+ /**
107
+ * Parse multiple secret overrides from CLI
108
+ *
109
+ * @param overrides - Array of override strings
110
+ * @returns Map of secret name to override URI
111
+ */
112
+ export function parseSecretOverrides(overrides: string[]): Record<string, string> {
113
+ const result: Record<string, string> = {};
114
+
115
+ for (const override of overrides) {
116
+ const parsed = parseSecretOverride(override);
117
+ if (parsed) {
118
+ result[parsed.name] = parsed.uri;
119
+ }
120
+ }
121
+
122
+ return result;
123
+ }
124
+
125
+ // Re-export URI functions
126
+ export { parseSecretUri, resolveSecretUri, isSecretUri, getSupportedSchemes } from "./uri-parser";
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Dagger Secret URI parser and resolver
3
+ *
4
+ * Supports secret URIs:
5
+ * - env://VAR_NAME - Environment variable
6
+ * - file://PATH - File contents
7
+ * - cmd://COMMAND - Command output
8
+ * - op://VAULT/ITEM/FIELD - 1Password
9
+ * - vault://PATH - HashiCorp Vault
10
+ */
11
+
12
+ import { execSync } from "node:child_process";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import type { DaggerSecretScheme, DaggerSecretUri } from "../types";
16
+
17
+ /**
18
+ * Valid URI schemes
19
+ */
20
+ const VALID_SCHEMES = new Set<DaggerSecretScheme>(["env", "file", "cmd", "op", "vault"]);
21
+
22
+ /**
23
+ * Parse a Dagger secret URI
24
+ *
25
+ * @param uri - The URI to parse (e.g., "env://API_KEY")
26
+ * @returns Parsed URI with scheme and value
27
+ * @throws Error if URI is invalid
28
+ */
29
+ export function parseSecretUri(uri: string): DaggerSecretUri {
30
+ const match = uri.match(/^([a-z]+):\/\/(.+)$/);
31
+ if (!match || !match[1] || !match[2]) {
32
+ throw new Error(`Invalid secret URI format: ${uri}. Expected format: scheme://value`);
33
+ }
34
+
35
+ const schemeStr = match[1];
36
+ const value = match[2];
37
+ const scheme = schemeStr as DaggerSecretScheme;
38
+
39
+ if (!VALID_SCHEMES.has(scheme)) {
40
+ throw new Error(
41
+ `Invalid secret URI scheme: ${scheme}. Valid schemes: ${Array.from(VALID_SCHEMES).join(", ")}`
42
+ );
43
+ }
44
+
45
+ return {
46
+ scheme,
47
+ value,
48
+ original: uri,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Check if a string is a valid secret URI
54
+ */
55
+ export function isSecretUri(uri: string): boolean {
56
+ try {
57
+ parseSecretUri(uri);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Resolve a secret URI to its actual value
66
+ *
67
+ * @param uri - The URI to resolve
68
+ * @returns The secret value
69
+ * @throws Error if resolution fails
70
+ */
71
+ export async function resolveSecretUri(uri: string): Promise<string> {
72
+ const parsed = parseSecretUri(uri);
73
+
74
+ switch (parsed.scheme) {
75
+ case "env":
76
+ return resolveEnvUri(parsed.value);
77
+ case "file":
78
+ return resolveFileUri(parsed.value);
79
+ case "cmd":
80
+ return resolveCmdUri(parsed.value);
81
+ case "op":
82
+ return resolveOpUri(parsed.value);
83
+ case "vault":
84
+ return resolveVaultUri(parsed.value);
85
+ default:
86
+ throw new Error(`Unsupported secret scheme: ${parsed.scheme}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Resolve env:// URI - read from environment variable
92
+ */
93
+ function resolveEnvUri(varName: string): string {
94
+ const value = process.env[varName];
95
+ if (value === undefined) {
96
+ throw new Error(`Environment variable '${varName}' is not set (from env://${varName})`);
97
+ }
98
+ return value;
99
+ }
100
+
101
+ /**
102
+ * Resolve file:// URI - read file contents
103
+ */
104
+ function resolveFileUri(path: string): string {
105
+ // Handle relative paths
106
+ const resolvedPath = resolve(path);
107
+
108
+ if (!existsSync(resolvedPath)) {
109
+ throw new Error(`File not found: ${resolvedPath} (from file://${path})`);
110
+ }
111
+
112
+ return readFileSync(resolvedPath, "utf-8").trim();
113
+ }
114
+
115
+ /**
116
+ * Resolve cmd:// URI - execute command and capture output
117
+ */
118
+ function resolveCmdUri(command: string): string {
119
+ try {
120
+ const output = execSync(command, {
121
+ encoding: "utf-8",
122
+ stdio: ["pipe", "pipe", "pipe"],
123
+ });
124
+ return output.trim();
125
+ } catch (error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ throw new Error(`Command failed: ${command} (from cmd://${command}): ${message}`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Resolve op:// URI - 1Password CLI
133
+ * Format: op://vault/item/field
134
+ *
135
+ * Requires 1Password CLI (op) to be installed and authenticated
136
+ */
137
+ function resolveOpUri(path: string): string {
138
+ const opUri = `op://${path}`;
139
+ try {
140
+ const output = execSync(`op read "${opUri}"`, {
141
+ encoding: "utf-8",
142
+ stdio: ["pipe", "pipe", "pipe"],
143
+ });
144
+ return output.trim();
145
+ } catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ throw new Error(
148
+ `1Password read failed for ${opUri}. Ensure 'op' CLI is installed and authenticated: ${message}`
149
+ );
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Resolve vault:// URI - HashiCorp Vault
155
+ * Format: vault://path/to/secret#field
156
+ *
157
+ * Requires Vault CLI and VAULT_ADDR, VAULT_TOKEN environment variables
158
+ */
159
+ function resolveVaultUri(path: string): string {
160
+ // Parse path and optional field
161
+ const [secretPath, field] = path.split("#");
162
+
163
+ try {
164
+ const command = `vault kv get -format=json "${secretPath}"`;
165
+ const output = execSync(command, {
166
+ encoding: "utf-8",
167
+ stdio: ["pipe", "pipe", "pipe"],
168
+ });
169
+
170
+ const data = JSON.parse(output);
171
+ const secretData = data.data?.data ?? data.data;
172
+
173
+ if (field) {
174
+ if (!(field in secretData)) {
175
+ throw new Error(`Field '${field}' not found in secret at ${secretPath}`);
176
+ }
177
+ return String(secretData[field]);
178
+ }
179
+
180
+ // If no field specified, return first value or JSON
181
+ const values = Object.values(secretData);
182
+ if (values.length === 1) {
183
+ return String(values[0]);
184
+ }
185
+ return JSON.stringify(secretData);
186
+ } catch (error) {
187
+ if (error instanceof SyntaxError) {
188
+ throw new Error(
189
+ `Failed to parse Vault response for ${path}. Ensure VAULT_ADDR and VAULT_TOKEN are set.`
190
+ );
191
+ }
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ throw new Error(`Vault read failed for ${path}: ${message}`);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Get supported URI schemes
199
+ */
200
+ export function getSupportedSchemes(): DaggerSecretScheme[] {
201
+ return Array.from(VALID_SCHEMES);
202
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Environment variable management
3
+ *
4
+ * Re-exports all env-related functions
5
+ */
6
+
7
+ // Parser functions
8
+ export {
9
+ parseEnvFile,
10
+ parseEnvContent,
11
+ serializeEnvFile,
12
+ createEnvContent,
13
+ updateEnvVar,
14
+ removeEnvVar,
15
+ } from "./parser";
16
+
17
+ // Reader functions
18
+ export {
19
+ getGlobalEnvPath,
20
+ getLocalEnvPath,
21
+ readEnvFile,
22
+ readEnvVars,
23
+ loadGlobalEnv,
24
+ loadLocalEnv,
25
+ loadGlobalEnvFile,
26
+ loadLocalEnvFile,
27
+ globalEnvExists,
28
+ localEnvExists,
29
+ } from "./reader";
30
+
31
+ // Writer functions
32
+ export {
33
+ writeEnvFile,
34
+ writeEnvVars,
35
+ setEnvVar,
36
+ deleteEnvVar,
37
+ setGlobalEnvVar,
38
+ setLocalEnvVar,
39
+ deleteGlobalEnvVar,
40
+ deleteLocalEnvVar,
41
+ } from "./writer";
42
+
43
+ // Manager functions (high-level API)
44
+ export {
45
+ setEnv,
46
+ getEnv,
47
+ getEnvValue,
48
+ deleteEnv,
49
+ listEnv,
50
+ resolveAllEnv,
51
+ resolveToolEnv,
52
+ hasLocalEnv,
53
+ hasGlobalEnv,
54
+ } from "./manager";