@kustodian/plugin-1password 1.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/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@kustodian/plugin-1password",
3
+ "version": "1.0.0",
4
+ "description": "1Password secret provider plugin for Kustodian",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "test": "bun test",
19
+ "test:watch": "bun test --watch",
20
+ "typecheck": "bun run tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "kustodian",
24
+ "plugin",
25
+ "1password",
26
+ "secrets",
27
+ "kubernetes"
28
+ ],
29
+ "author": "Luca Silverentand <luca@onezero.company>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/lucasilverentand/kustodian.git",
34
+ "directory": "plugins/1password"
35
+ },
36
+ "publishConfig": {
37
+ "registry": "https://npm.pkg.github.com"
38
+ },
39
+ "dependencies": {
40
+ "@kustodian/core": "workspace:*",
41
+ "@kustodian/plugins": "workspace:*",
42
+ "@kustodian/schema": "workspace:*"
43
+ },
44
+ "devDependencies": {}
45
+ }
@@ -0,0 +1,190 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ import { Errors, type ResultType, failure, success } from '@kustodian/core';
5
+ import type { KustodianErrorType } from '@kustodian/core';
6
+
7
+ import { DEFAULT_TIMEOUT, type OnePasswordPluginOptionsType } from './types.js';
8
+
9
+ const exec_file_async = promisify(execFile);
10
+
11
+ /**
12
+ * Result of a command execution.
13
+ */
14
+ export interface CommandResultType {
15
+ stdout: string;
16
+ stderr: string;
17
+ exit_code: number;
18
+ }
19
+
20
+ /**
21
+ * Options for command execution.
22
+ */
23
+ export interface ExecOptionsType {
24
+ cwd?: string | undefined;
25
+ timeout?: number | undefined;
26
+ env?: Record<string, string> | undefined;
27
+ }
28
+
29
+ /**
30
+ * Executes a command and returns the result.
31
+ * Uses execFile instead of exec for security (no shell invocation).
32
+ */
33
+ export async function exec_command(
34
+ command: string,
35
+ args: string[] = [],
36
+ options: ExecOptionsType = {},
37
+ ): Promise<ResultType<CommandResultType, KustodianErrorType>> {
38
+ try {
39
+ const { stdout, stderr } = await exec_file_async(command, args, {
40
+ cwd: options.cwd,
41
+ timeout: options.timeout,
42
+ env: { ...process.env, ...options.env },
43
+ });
44
+
45
+ return success({
46
+ stdout,
47
+ stderr,
48
+ exit_code: 0,
49
+ });
50
+ } catch (error) {
51
+ if (is_exec_error(error)) {
52
+ return success({
53
+ stdout: error.stdout ?? '',
54
+ stderr: error.stderr ?? '',
55
+ exit_code: error.code ?? 1,
56
+ });
57
+ }
58
+
59
+ return failure(Errors.unknown(`Failed to execute command: ${command}`, error));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Type guard for exec errors.
65
+ */
66
+ function is_exec_error(
67
+ error: unknown,
68
+ ): error is { stdout?: string; stderr?: string; code?: number } {
69
+ return (
70
+ typeof error === 'object' &&
71
+ error !== null &&
72
+ ('stdout' in error || 'stderr' in error || 'code' in error)
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Gets environment variables for 1Password CLI authentication.
78
+ */
79
+ function get_op_auth_env(options: OnePasswordPluginOptionsType): Record<string, string> {
80
+ const env: Record<string, string> = {};
81
+
82
+ const token = options.service_account_token ?? process.env['OP_SERVICE_ACCOUNT_TOKEN'];
83
+ if (token) {
84
+ env['OP_SERVICE_ACCOUNT_TOKEN'] = token;
85
+ }
86
+
87
+ return env;
88
+ }
89
+
90
+ /**
91
+ * Checks if op CLI is available in the system PATH.
92
+ */
93
+ export async function check_op_available(): Promise<ResultType<string, KustodianErrorType>> {
94
+ const result = await exec_command('op', ['--version']);
95
+
96
+ if (!result.success) {
97
+ return result;
98
+ }
99
+
100
+ if (result.value.exit_code !== 0) {
101
+ return failure(Errors.secret_cli_not_found('1Password', 'op'));
102
+ }
103
+
104
+ // Parse version from output (e.g., "2.30.0")
105
+ const version = result.value.stdout.trim();
106
+ return success(version);
107
+ }
108
+
109
+ /**
110
+ * Reads a single secret from 1Password.
111
+ */
112
+ export async function op_read(
113
+ ref: string,
114
+ options: OnePasswordPluginOptionsType = {},
115
+ ): Promise<ResultType<string, KustodianErrorType>> {
116
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
117
+ const env = get_op_auth_env(options);
118
+
119
+ const result = await exec_command('op', ['read', ref], {
120
+ timeout,
121
+ env,
122
+ });
123
+
124
+ if (!result.success) {
125
+ return result;
126
+ }
127
+
128
+ const { stdout, stderr, exit_code } = result.value;
129
+
130
+ if (exit_code !== 0) {
131
+ // Parse specific error types from stderr
132
+ const stderr_lower = stderr.toLowerCase();
133
+
134
+ if (
135
+ stderr_lower.includes('not found') ||
136
+ stderr_lower.includes("doesn't exist") ||
137
+ stderr_lower.includes('no item')
138
+ ) {
139
+ return failure(Errors.secret_not_found('1Password', ref));
140
+ }
141
+
142
+ if (
143
+ stderr_lower.includes('sign in') ||
144
+ stderr_lower.includes('authentication') ||
145
+ stderr_lower.includes('unauthorized') ||
146
+ stderr_lower.includes('not signed in')
147
+ ) {
148
+ return failure(Errors.secret_auth_error('1Password', stderr));
149
+ }
150
+
151
+ if (stderr_lower.includes('timeout')) {
152
+ return failure(Errors.secret_timeout('1Password', timeout));
153
+ }
154
+
155
+ return failure(Errors.unknown(`1Password error: ${stderr}`));
156
+ }
157
+
158
+ return success(stdout.trim());
159
+ }
160
+
161
+ /**
162
+ * Reads multiple secrets from 1Password in batch.
163
+ */
164
+ export async function op_read_batch(
165
+ refs: string[],
166
+ options: OnePasswordPluginOptionsType = {},
167
+ ): Promise<ResultType<Record<string, string>, KustodianErrorType>> {
168
+ if (refs.length === 0) {
169
+ return success({});
170
+ }
171
+
172
+ const results: Record<string, string> = {};
173
+
174
+ // Read secrets sequentially for now
175
+ for (const ref of refs) {
176
+ const result = await op_read(ref, options);
177
+
178
+ if (!result.success) {
179
+ if (options.fail_on_missing !== false) {
180
+ return result;
181
+ }
182
+ // Skip missing secrets if fail_on_missing is false
183
+ continue;
184
+ }
185
+
186
+ results[ref] = result.value;
187
+ }
188
+
189
+ return success(results);
190
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Plugin exports
2
+ export { create_onepassword_plugin, plugin } from './plugin.js';
3
+ export { plugin as default } from './plugin.js';
4
+
5
+ // Executor exports
6
+ export {
7
+ check_op_available,
8
+ exec_command,
9
+ op_read,
10
+ op_read_batch,
11
+ type CommandResultType,
12
+ type ExecOptionsType,
13
+ } from './executor.js';
14
+
15
+ // Resolver exports
16
+ export { resolve_onepassword_substitutions } from './resolver.js';
17
+
18
+ // Types
19
+ export {
20
+ parse_onepassword_ref,
21
+ DEFAULT_TIMEOUT,
22
+ type OnePasswordPluginOptionsType,
23
+ type OnePasswordRefType,
24
+ } from './types.js';
package/src/plugin.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { success } from '@kustodian/core';
2
+ import type {
3
+ CommandType,
4
+ HookContextType,
5
+ HookEventType,
6
+ KustodianPluginType,
7
+ PluginCommandContributionType,
8
+ PluginHookContributionType,
9
+ PluginManifestType,
10
+ } from '@kustodian/plugins';
11
+
12
+ import { check_op_available, op_read } from './executor.js';
13
+ import type { OnePasswordPluginOptionsType } from './types.js';
14
+
15
+ /**
16
+ * 1Password plugin manifest.
17
+ */
18
+ const manifest: PluginManifestType = {
19
+ name: '@kustodian/plugin-1password',
20
+ version: '0.1.0',
21
+ description: '1Password secret provider for Kustodian',
22
+ capabilities: ['commands', 'hooks'],
23
+ };
24
+
25
+ /**
26
+ * Creates the 1Password plugin.
27
+ */
28
+ export function create_onepassword_plugin(
29
+ options: OnePasswordPluginOptionsType = {},
30
+ ): KustodianPluginType {
31
+ return {
32
+ manifest,
33
+
34
+ async activate() {
35
+ // Verify CLI availability on activation (warning only)
36
+ const check_result = await check_op_available();
37
+ if (!check_result.success) {
38
+ console.warn('1Password CLI (op) not found - secret resolution may fail');
39
+ }
40
+ return success(undefined);
41
+ },
42
+
43
+ async deactivate() {
44
+ return success(undefined);
45
+ },
46
+
47
+ get_commands(): PluginCommandContributionType[] {
48
+ const onepassword_command: CommandType = {
49
+ name: '1password',
50
+ description: '1Password secret management commands',
51
+ subcommands: [
52
+ {
53
+ name: 'check',
54
+ description: 'Check 1Password CLI availability and authentication',
55
+ handler: async () => {
56
+ const result = await check_op_available();
57
+ if (result.success) {
58
+ console.log(`1Password CLI version: ${result.value}`);
59
+ return success(undefined);
60
+ }
61
+ console.error('1Password CLI not available');
62
+ return result;
63
+ },
64
+ },
65
+ {
66
+ name: 'test',
67
+ description: 'Test reading a secret reference',
68
+ arguments: [
69
+ {
70
+ name: 'ref',
71
+ description: 'Secret reference (op://vault/item/field)',
72
+ required: true,
73
+ },
74
+ ],
75
+ handler: async (ctx) => {
76
+ const ref = ctx.args[0];
77
+ if (!ref) {
78
+ console.error('Missing secret reference');
79
+ return success(undefined);
80
+ }
81
+
82
+ const result = await op_read(ref, options);
83
+ if (result.success) {
84
+ console.log('Secret retrieved successfully (value hidden)');
85
+ console.log(`Length: ${result.value.length} characters`);
86
+ return success(undefined);
87
+ }
88
+
89
+ console.error(`Failed to read secret: ${result.error.message}`);
90
+ return result;
91
+ },
92
+ },
93
+ ],
94
+ };
95
+ return [{ command: onepassword_command }];
96
+ },
97
+
98
+ get_hooks(): PluginHookContributionType[] {
99
+ return [
100
+ {
101
+ event: 'generator:after_resolve',
102
+ priority: 50, // Run before default (100) to inject secrets early
103
+ handler: async (_event: HookEventType, ctx: HookContextType) => {
104
+ // Hook for secret injection will be implemented in generator integration
105
+ return success(ctx);
106
+ },
107
+ },
108
+ ];
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Default plugin export.
115
+ */
116
+ export const plugin = create_onepassword_plugin();
117
+
118
+ export default plugin;
@@ -0,0 +1,46 @@
1
+ import { type ResultType, failure, success } from '@kustodian/core';
2
+ import type { KustodianErrorType } from '@kustodian/core';
3
+ import type { OnePasswordSubstitutionType } from '@kustodian/schema';
4
+
5
+ import { op_read } from './executor.js';
6
+ import type { OnePasswordPluginOptionsType } from './types.js';
7
+
8
+ /**
9
+ * Resolves 1Password substitutions to actual secret values.
10
+ * Returns a map from substitution name to resolved value.
11
+ */
12
+ export async function resolve_onepassword_substitutions(
13
+ substitutions: OnePasswordSubstitutionType[],
14
+ options: OnePasswordPluginOptionsType = {},
15
+ ): Promise<ResultType<Record<string, string>, KustodianErrorType>> {
16
+ if (substitutions.length === 0) {
17
+ return success({});
18
+ }
19
+
20
+ const results: Record<string, string> = {};
21
+
22
+ for (const sub of substitutions) {
23
+ const result = await op_read(sub.ref, options);
24
+
25
+ if (!result.success) {
26
+ // If we have a default and fail_on_missing is false, use the default
27
+ if (sub.default !== undefined && options.fail_on_missing === false) {
28
+ results[sub.name] = sub.default;
29
+ continue;
30
+ }
31
+
32
+ // If we have a default, use it
33
+ if (sub.default !== undefined) {
34
+ results[sub.name] = sub.default;
35
+ continue;
36
+ }
37
+
38
+ // Otherwise, propagate the error
39
+ return failure(result.error);
40
+ }
41
+
42
+ results[sub.name] = result.value;
43
+ }
44
+
45
+ return success(results);
46
+ }
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Options for the 1Password plugin.
3
+ */
4
+ export interface OnePasswordPluginOptionsType {
5
+ /** Service account token (can also be set via OP_SERVICE_ACCOUNT_TOKEN env var) */
6
+ service_account_token?: string | undefined;
7
+ /** Timeout for CLI operations in milliseconds (default: 30000) */
8
+ timeout?: number | undefined;
9
+ /** Whether to fail on missing secrets (default: true) */
10
+ fail_on_missing?: boolean | undefined;
11
+ }
12
+
13
+ /**
14
+ * Parsed 1Password reference.
15
+ */
16
+ export interface OnePasswordRefType {
17
+ vault: string;
18
+ item: string;
19
+ section?: string | undefined;
20
+ field: string;
21
+ }
22
+
23
+ /**
24
+ * Default timeout for 1Password CLI operations.
25
+ */
26
+ export const DEFAULT_TIMEOUT = 30000;
27
+
28
+ /**
29
+ * Parses a 1Password secret reference.
30
+ * Format: op://vault/item[/section]/field
31
+ */
32
+ export function parse_onepassword_ref(ref: string): OnePasswordRefType | undefined {
33
+ // Match: op://vault/item/field or op://vault/item/section/field
34
+ const match = ref.match(/^op:\/\/([^/]+)\/([^/]+)\/(?:([^/]+)\/)?([^/]+)$/);
35
+
36
+ if (!match) {
37
+ return undefined;
38
+ }
39
+
40
+ const [, vault, item, section, field] = match;
41
+
42
+ // If section is undefined, the field is in position 3
43
+ if (field === undefined) {
44
+ return {
45
+ vault: vault!,
46
+ item: item!,
47
+ field: section!,
48
+ };
49
+ }
50
+
51
+ return {
52
+ vault: vault!,
53
+ item: item!,
54
+ section,
55
+ field,
56
+ };
57
+ }