@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/src/index.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @enactprotocol/secrets
3
+ *
4
+ * OS keyring integration and environment variable management for Enact.
5
+ * Provides secure secret storage using platform-native keychains.
6
+ */
7
+
8
+ export const version = "0.1.0";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type {
15
+ SecretResolution,
16
+ SecretNotFound,
17
+ SecretResolutionResult,
18
+ SecretMetadata,
19
+ SecretTrace,
20
+ SecretTraceEntry,
21
+ EnvScope,
22
+ EnvFileLocation,
23
+ EnvironmentVariable,
24
+ EnvResolution,
25
+ ParsedEnvFile,
26
+ EnvFileLine,
27
+ DaggerSecretScheme,
28
+ DaggerSecretUri,
29
+ GetSecretOptions,
30
+ SecretObject,
31
+ } from "./types";
32
+
33
+ export { KEYRING_SERVICE } from "./types";
34
+
35
+ // ============================================================================
36
+ // Keyring Functions
37
+ // ============================================================================
38
+
39
+ export {
40
+ buildAccount,
41
+ parseAccount,
42
+ setSecret,
43
+ getSecret,
44
+ deleteSecret,
45
+ listSecrets,
46
+ listAllSecrets,
47
+ secretExists,
48
+ isKeyringAvailable,
49
+ } from "./keyring";
50
+
51
+ // ============================================================================
52
+ // Secret Resolution
53
+ // ============================================================================
54
+
55
+ export {
56
+ getNamespaceChain,
57
+ resolveSecret,
58
+ traceSecretResolution,
59
+ resolveSecrets,
60
+ checkRequiredSecrets,
61
+ } from "./resolver";
62
+
63
+ // ============================================================================
64
+ // Environment Variables
65
+ // ============================================================================
66
+
67
+ export {
68
+ // Parser
69
+ parseEnvFile,
70
+ parseEnvContent,
71
+ serializeEnvFile,
72
+ createEnvContent,
73
+ updateEnvVar,
74
+ removeEnvVar,
75
+ // Reader
76
+ getGlobalEnvPath,
77
+ getLocalEnvPath,
78
+ readEnvFile,
79
+ readEnvVars,
80
+ loadGlobalEnv,
81
+ loadLocalEnv,
82
+ loadGlobalEnvFile,
83
+ loadLocalEnvFile,
84
+ globalEnvExists,
85
+ localEnvExists,
86
+ // Writer
87
+ writeEnvFile,
88
+ writeEnvVars,
89
+ setEnvVar,
90
+ deleteEnvVar,
91
+ setGlobalEnvVar,
92
+ setLocalEnvVar,
93
+ deleteGlobalEnvVar,
94
+ deleteLocalEnvVar,
95
+ // Manager (high-level API)
96
+ setEnv,
97
+ getEnv,
98
+ getEnvValue,
99
+ deleteEnv,
100
+ listEnv,
101
+ resolveAllEnv,
102
+ resolveToolEnv,
103
+ hasLocalEnv,
104
+ hasGlobalEnv,
105
+ } from "./env";
106
+
107
+ // ============================================================================
108
+ // Dagger Secret Integration
109
+ // ============================================================================
110
+
111
+ export {
112
+ parseSecretUri,
113
+ resolveSecretUri,
114
+ isSecretUri,
115
+ getSupportedSchemes,
116
+ getSecretObject,
117
+ getSecretObjects,
118
+ parseSecretOverride,
119
+ parseSecretOverrides,
120
+ } from "./dagger";
package/src/keyring.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * OS Keyring integration for secure secret storage
3
+ *
4
+ * Uses the system keychain:
5
+ * - macOS: Keychain
6
+ * - Windows: Credential Manager
7
+ * - Linux: Secret Service (libsecret)
8
+ *
9
+ * All secrets are stored with:
10
+ * - Service: "enact-cli"
11
+ * - Account: "{namespace}:{SECRET_NAME}"
12
+ */
13
+
14
+ import { keyring } from "@zowe/secrets-for-zowe-sdk";
15
+ import { KEYRING_SERVICE, type SecretMetadata } from "./types";
16
+
17
+ /**
18
+ * Build the account string for keyring storage
19
+ * Format: "namespace:SECRET_NAME"
20
+ */
21
+ export function buildAccount(namespace: string, secretName: string): string {
22
+ return `${namespace}:${secretName}`;
23
+ }
24
+
25
+ /**
26
+ * Parse an account string back to namespace and secret name
27
+ */
28
+ export function parseAccount(account: string): {
29
+ namespace: string;
30
+ secretName: string;
31
+ } {
32
+ const colonIndex = account.lastIndexOf(":");
33
+ if (colonIndex === -1) {
34
+ throw new Error(`Invalid account format: ${account}`);
35
+ }
36
+ return {
37
+ namespace: account.slice(0, colonIndex),
38
+ secretName: account.slice(colonIndex + 1),
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Store a secret in the OS keyring
44
+ *
45
+ * @param namespace - The namespace for the secret (e.g., "alice/api")
46
+ * @param name - The secret name (e.g., "API_TOKEN")
47
+ * @param value - The secret value to store
48
+ */
49
+ export async function setSecret(namespace: string, name: string, value: string): Promise<void> {
50
+ const account = buildAccount(namespace, name);
51
+ await keyring.setPassword(KEYRING_SERVICE, account, value);
52
+ }
53
+
54
+ /**
55
+ * Retrieve a secret from the OS keyring
56
+ *
57
+ * @param namespace - The namespace for the secret
58
+ * @param name - The secret name
59
+ * @returns The secret value, or null if not found
60
+ */
61
+ export async function getSecret(namespace: string, name: string): Promise<string | null> {
62
+ const account = buildAccount(namespace, name);
63
+ const value = await keyring.getPassword(KEYRING_SERVICE, account);
64
+ return value ?? null;
65
+ }
66
+
67
+ /**
68
+ * Delete a secret from the OS keyring
69
+ *
70
+ * @param namespace - The namespace for the secret
71
+ * @param name - The secret name
72
+ * @returns true if deleted, false if not found
73
+ */
74
+ export async function deleteSecret(namespace: string, name: string): Promise<boolean> {
75
+ const account = buildAccount(namespace, name);
76
+ return await keyring.deletePassword(KEYRING_SERVICE, account);
77
+ }
78
+
79
+ /**
80
+ * List all secrets for a namespace
81
+ *
82
+ * @param namespace - The namespace to list secrets for
83
+ * @returns Array of secret names in the namespace
84
+ */
85
+ export async function listSecrets(namespace: string): Promise<string[]> {
86
+ const credentials = await keyring.findCredentials(KEYRING_SERVICE);
87
+ const prefix = `${namespace}:`;
88
+
89
+ return credentials
90
+ .filter((cred) => cred.account.startsWith(prefix))
91
+ .map((cred) => cred.account.slice(prefix.length));
92
+ }
93
+
94
+ /**
95
+ * List all secrets across all namespaces
96
+ *
97
+ * @returns Array of secret metadata
98
+ */
99
+ export async function listAllSecrets(): Promise<SecretMetadata[]> {
100
+ const credentials = await keyring.findCredentials(KEYRING_SERVICE);
101
+
102
+ return credentials.map((cred) => {
103
+ const { namespace, secretName } = parseAccount(cred.account);
104
+ return {
105
+ key: secretName,
106
+ namespace,
107
+ };
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Check if a secret exists in the keyring
113
+ *
114
+ * @param namespace - The namespace for the secret
115
+ * @param name - The secret name
116
+ * @returns true if the secret exists
117
+ */
118
+ export async function secretExists(namespace: string, name: string): Promise<boolean> {
119
+ const value = await getSecret(namespace, name);
120
+ return value !== null;
121
+ }
122
+
123
+ /**
124
+ * Check if the keyring is available on this system
125
+ *
126
+ * @returns true if keyring operations are available
127
+ */
128
+ export async function isKeyringAvailable(): Promise<boolean> {
129
+ try {
130
+ // Try to list credentials - this will fail if keyring is not available
131
+ await keyring.findCredentials(KEYRING_SERVICE);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Secret resolution with namespace inheritance
3
+ *
4
+ * When a tool requests a secret, we walk up the namespace path:
5
+ * Tool: alice/api/slack/notifier
6
+ * Needs: API_TOKEN
7
+ *
8
+ * Lookup:
9
+ * 1. alice/api/slack:API_TOKEN
10
+ * 2. alice/api:API_TOKEN ✓ found
11
+ * 3. alice:API_TOKEN
12
+ *
13
+ * First match wins.
14
+ */
15
+
16
+ import { getSecret } from "./keyring";
17
+ import type {
18
+ SecretResolution,
19
+ SecretResolutionResult,
20
+ SecretTrace,
21
+ SecretTraceEntry,
22
+ } from "./types";
23
+
24
+ /**
25
+ * Get the namespace chain for a tool path
26
+ * Walks up the path segments from most specific to least specific
27
+ *
28
+ * @param toolPath - The full tool path (e.g., "alice/api/slack")
29
+ * @returns Array of namespaces to check in order
30
+ *
31
+ * @example
32
+ * getNamespaceChain("alice/api/slack")
33
+ * // Returns: ["alice/api/slack", "alice/api", "alice"]
34
+ */
35
+ export function getNamespaceChain(toolPath: string): string[] {
36
+ const segments = toolPath.split("/").filter(Boolean);
37
+ const chain: string[] = [];
38
+
39
+ for (let i = segments.length; i > 0; i--) {
40
+ chain.push(segments.slice(0, i).join("/"));
41
+ }
42
+
43
+ return chain;
44
+ }
45
+
46
+ /**
47
+ * Resolve a secret using namespace inheritance
48
+ *
49
+ * @param toolPath - The tool path to resolve secrets for
50
+ * @param secretName - The secret name to find
51
+ * @returns Resolution result with namespace info, or not-found result
52
+ */
53
+ export async function resolveSecret(
54
+ toolPath: string,
55
+ secretName: string
56
+ ): Promise<SecretResolutionResult> {
57
+ const namespaces = getNamespaceChain(toolPath);
58
+ const searchedNamespaces: string[] = [];
59
+
60
+ for (const namespace of namespaces) {
61
+ searchedNamespaces.push(namespace);
62
+ const value = await getSecret(namespace, secretName);
63
+
64
+ if (value !== null) {
65
+ return {
66
+ namespace,
67
+ value,
68
+ key: secretName,
69
+ found: true,
70
+ };
71
+ }
72
+ }
73
+
74
+ return {
75
+ key: secretName,
76
+ found: false,
77
+ searchedNamespaces,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Trace secret resolution for debugging
83
+ * Shows which namespaces were checked and where the secret was found
84
+ *
85
+ * @param toolPath - The tool path to resolve secrets for
86
+ * @param secretName - The secret name to find
87
+ * @returns Full trace with all namespaces checked
88
+ */
89
+ export async function traceSecretResolution(
90
+ toolPath: string,
91
+ secretName: string
92
+ ): Promise<SecretTrace> {
93
+ const namespaces = getNamespaceChain(toolPath);
94
+ const entries: SecretTraceEntry[] = [];
95
+ let result: SecretResolutionResult | null = null;
96
+
97
+ for (const namespace of namespaces) {
98
+ const account = `${namespace}:${secretName}`;
99
+ const value = await getSecret(namespace, secretName);
100
+ const found = value !== null;
101
+
102
+ entries.push({ namespace, account, found });
103
+
104
+ if (found && !result) {
105
+ result = {
106
+ namespace,
107
+ value,
108
+ key: secretName,
109
+ found: true,
110
+ };
111
+ }
112
+ }
113
+
114
+ // If not found anywhere
115
+ if (!result) {
116
+ result = {
117
+ key: secretName,
118
+ found: false,
119
+ searchedNamespaces: namespaces,
120
+ };
121
+ }
122
+
123
+ return {
124
+ key: secretName,
125
+ toolPath,
126
+ entries,
127
+ result,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Resolve multiple secrets for a tool
133
+ *
134
+ * @param toolPath - The tool path to resolve secrets for
135
+ * @param secretNames - Array of secret names to resolve
136
+ * @returns Map of secret name to resolution result
137
+ */
138
+ export async function resolveSecrets(
139
+ toolPath: string,
140
+ secretNames: string[]
141
+ ): Promise<Map<string, SecretResolutionResult>> {
142
+ const results = new Map<string, SecretResolutionResult>();
143
+
144
+ for (const name of secretNames) {
145
+ const result = await resolveSecret(toolPath, name);
146
+ results.set(name, result);
147
+ }
148
+
149
+ return results;
150
+ }
151
+
152
+ /**
153
+ * Check if all required secrets are available for a tool
154
+ *
155
+ * @param toolPath - The tool path to check
156
+ * @param requiredSecrets - Array of required secret names
157
+ * @returns Object with available and missing secrets
158
+ */
159
+ export async function checkRequiredSecrets(
160
+ toolPath: string,
161
+ requiredSecrets: string[]
162
+ ): Promise<{
163
+ allFound: boolean;
164
+ found: SecretResolution[];
165
+ missing: string[];
166
+ }> {
167
+ const found: SecretResolution[] = [];
168
+ const missing: string[] = [];
169
+
170
+ for (const name of requiredSecrets) {
171
+ const result = await resolveSecret(toolPath, name);
172
+ if (result.found) {
173
+ found.push(result);
174
+ } else {
175
+ missing.push(name);
176
+ }
177
+ }
178
+
179
+ return {
180
+ allFound: missing.length === 0,
181
+ found,
182
+ missing,
183
+ };
184
+ }
package/src/types.ts ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Type definitions for secrets and environment management
3
+ */
4
+
5
+ // ============================================================================
6
+ // Constants
7
+ // ============================================================================
8
+
9
+ /** Keyring service name for all Enact secrets */
10
+ export const KEYRING_SERVICE = "enact-cli";
11
+
12
+ // ============================================================================
13
+ // Secret Types
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Result of secret resolution with namespace information
18
+ */
19
+ export interface SecretResolution {
20
+ /** The namespace where the secret was found */
21
+ namespace: string;
22
+ /** The secret value */
23
+ value: string;
24
+ /** The secret key */
25
+ key: string;
26
+ /** Whether the secret was found */
27
+ found: true;
28
+ }
29
+
30
+ /**
31
+ * Result when secret is not found
32
+ */
33
+ export interface SecretNotFound {
34
+ /** The secret key that was looked for */
35
+ key: string;
36
+ /** Whether the secret was found */
37
+ found: false;
38
+ /** Namespaces that were searched */
39
+ searchedNamespaces: string[];
40
+ }
41
+
42
+ /**
43
+ * Result of secret resolution (found or not found)
44
+ */
45
+ export type SecretResolutionResult = SecretResolution | SecretNotFound;
46
+
47
+ /**
48
+ * Metadata about a stored secret
49
+ */
50
+ export interface SecretMetadata {
51
+ /** Secret key name */
52
+ key: string;
53
+ /** Namespace where it's stored */
54
+ namespace: string;
55
+ /** When it was created/last modified (if available) */
56
+ modified?: Date;
57
+ }
58
+
59
+ /**
60
+ * Trace entry for debugging secret resolution
61
+ */
62
+ export interface SecretTraceEntry {
63
+ /** Namespace that was checked */
64
+ namespace: string;
65
+ /** Account string used for lookup */
66
+ account: string;
67
+ /** Whether secret was found at this namespace */
68
+ found: boolean;
69
+ }
70
+
71
+ /**
72
+ * Full trace of secret resolution
73
+ */
74
+ export interface SecretTrace {
75
+ /** Secret key being resolved */
76
+ key: string;
77
+ /** Tool path used for resolution */
78
+ toolPath: string;
79
+ /** Each namespace checked in order */
80
+ entries: SecretTraceEntry[];
81
+ /** Final result */
82
+ result: SecretResolutionResult;
83
+ }
84
+
85
+ // ============================================================================
86
+ // Environment Variable Types
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Scope where environment variables can be stored
91
+ */
92
+ export type EnvScope = "global" | "local" | "default";
93
+
94
+ /**
95
+ * Location where environment variables can be stored (files only)
96
+ */
97
+ export type EnvFileLocation = "global" | "local";
98
+
99
+ /**
100
+ * An environment variable with its value and source
101
+ */
102
+ export interface EnvironmentVariable {
103
+ /** Variable key */
104
+ key: string;
105
+ /** Variable value */
106
+ value: string;
107
+ /** Where it was resolved from */
108
+ source: EnvScope;
109
+ }
110
+
111
+ /**
112
+ * Result of environment variable resolution
113
+ */
114
+ export interface EnvResolution {
115
+ /** Variable key */
116
+ key: string;
117
+ /** Resolved value */
118
+ value: string;
119
+ /** Source of the value */
120
+ source: EnvScope;
121
+ /** File path if from a file */
122
+ filePath?: string;
123
+ }
124
+
125
+ /**
126
+ * Parsed .env file content
127
+ */
128
+ export interface ParsedEnvFile {
129
+ /** Key-value pairs */
130
+ vars: Record<string, string>;
131
+ /** Preserved lines (for writing back with comments) */
132
+ lines: EnvFileLine[];
133
+ }
134
+
135
+ /**
136
+ * A line in an .env file (for preserving format)
137
+ */
138
+ export interface EnvFileLine {
139
+ /** Type of line */
140
+ type: "comment" | "empty" | "variable";
141
+ /** Original line content */
142
+ raw: string;
143
+ /** Variable key (if type is 'variable') */
144
+ key?: string;
145
+ /** Variable value (if type is 'variable') */
146
+ value?: string;
147
+ }
148
+
149
+ // ============================================================================
150
+ // Dagger Secret URI Types
151
+ // ============================================================================
152
+
153
+ /**
154
+ * Supported Dagger secret URI schemes
155
+ */
156
+ export type DaggerSecretScheme = "env" | "file" | "cmd" | "op" | "vault";
157
+
158
+ /**
159
+ * Parsed Dagger secret URI
160
+ */
161
+ export interface DaggerSecretUri {
162
+ /** The URI scheme */
163
+ scheme: DaggerSecretScheme;
164
+ /** The URI value (without scheme://) */
165
+ value: string;
166
+ /** Original full URI */
167
+ original: string;
168
+ }
169
+
170
+ /**
171
+ * Options for getting a secret object
172
+ */
173
+ export interface GetSecretOptions {
174
+ /** Override URI to use instead of keyring */
175
+ overrideUri?: string;
176
+ /** Whether to trace resolution */
177
+ trace?: boolean;
178
+ }
179
+
180
+ /**
181
+ * Result from getSecretObject
182
+ */
183
+ export interface SecretObject {
184
+ /** The secret name */
185
+ name: string;
186
+ /** The secret value */
187
+ value: string;
188
+ /** Source of the secret */
189
+ source: "keyring" | "override";
190
+ /** Namespace if from keyring */
191
+ namespace?: string;
192
+ /** Override URI if used */
193
+ overrideUri?: string;
194
+ }