@majkapp/plugin-kit 3.7.2 → 3.7.4

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.
@@ -0,0 +1,412 @@
1
+ # Secrets API
2
+
3
+ The `ctx.majk.secrets` API provides secure storage and retrieval of sensitive information like API keys, tokens, and credentials. Secrets are scoped to global, project, or integration levels for proper isolation and security.
4
+
5
+ ## Interface
6
+
7
+ ```typescript
8
+ export interface SecretsAPI {
9
+ set(key: string, value: string, scope?: SecretScope): Promise<void>;
10
+ get(key: string, scope?: SecretScope): Promise<string | null>;
11
+ has(key: string, scope?: SecretScope): Promise<boolean>;
12
+ list(scope?: SecretScope): Promise<SecretInfo[]>;
13
+ delete(key: string, scope?: SecretScope): Promise<boolean>;
14
+ forProject(projectId: string): ScopedSecretsAPI;
15
+ forIntegration(integrationId: string): ScopedSecretsAPI;
16
+ }
17
+
18
+ export interface SecretScope {
19
+ type: 'global' | 'project' | 'integration';
20
+ id?: string;
21
+ }
22
+
23
+ export interface SecretInfo {
24
+ key: string;
25
+ scope: SecretScope;
26
+ description?: string;
27
+ createdAt: Date;
28
+ updatedAt: Date;
29
+ lastUsedAt?: Date;
30
+ tags?: string[];
31
+ }
32
+
33
+ export interface ScopedSecretsAPI {
34
+ set(key: string, value: string): Promise<void>;
35
+ get(key: string): Promise<string | null>;
36
+ has(key: string): Promise<boolean>;
37
+ list(): Promise<string[]>;
38
+ delete(key: string): Promise<boolean>;
39
+ }
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```typescript
45
+ // Store an API key globally
46
+ await ctx.majk.secrets.set('openai.apiKey', 'sk-...', { type: 'global' });
47
+
48
+ // Retrieve the API key
49
+ const apiKey = await ctx.majk.secrets.get('openai.apiKey', { type: 'global' });
50
+
51
+ // Store project-specific secrets
52
+ await ctx.majk.secrets.set('database.password', 'secret123', {
53
+ type: 'project',
54
+ id: 'my-project-id'
55
+ });
56
+
57
+ // Use scoped API for cleaner code
58
+ const projectSecrets = ctx.majk.secrets.forProject('my-project-id');
59
+ await projectSecrets.set('aws.accessKey', 'AKIA...');
60
+ const accessKey = await projectSecrets.get('aws.accessKey');
61
+ ```
62
+
63
+ ## Methods
64
+
65
+ ### `set(key: string, value: string, scope?: SecretScope): Promise<void>`
66
+
67
+ Stores a secret value securely. If no scope is provided, defaults to global scope.
68
+
69
+ ```typescript
70
+ // Global secret (available across all projects)
71
+ await ctx.majk.secrets.set('github.token', 'ghp_xxxxxxxxxxxx', { type: 'global' });
72
+
73
+ // Project-specific secret
74
+ await ctx.majk.secrets.set('api.key', 'abc123', {
75
+ type: 'project',
76
+ id: 'ecommerce-app'
77
+ });
78
+
79
+ // Integration-specific secret
80
+ await ctx.majk.secrets.set('webhook.secret', 'wh_secret', {
81
+ type: 'integration',
82
+ id: 'slack-integration'
83
+ });
84
+ ```
85
+
86
+ ### `get(key: string, scope?: SecretScope): Promise<string | null>`
87
+
88
+ Retrieves a secret value. Returns `null` if the secret doesn't exist.
89
+
90
+ ```typescript
91
+ // Get global secret
92
+ const githubToken = await ctx.majk.secrets.get('github.token', { type: 'global' });
93
+
94
+ // Get project secret with fallback
95
+ const apiKey = await ctx.majk.secrets.get('api.key', { type: 'project', id: 'my-project' }) || 'default-key';
96
+
97
+ // Check if secret exists before using
98
+ if (await ctx.majk.secrets.has('openai.apiKey')) {
99
+ const openaiKey = await ctx.majk.secrets.get('openai.apiKey');
100
+ // Use the key safely
101
+ }
102
+ ```
103
+
104
+ ### `has(key: string, scope?: SecretScope): Promise<boolean>`
105
+
106
+ Checks if a secret exists without retrieving its value.
107
+
108
+ ```typescript
109
+ // Check for required secrets before starting operation
110
+ const requiredSecrets = ['aws.accessKey', 'aws.secretKey', 'aws.region'];
111
+ const projectId = 'deployment-project';
112
+
113
+ for (const secret of requiredSecrets) {
114
+ if (!await ctx.majk.secrets.has(secret, { type: 'project', id: projectId })) {
115
+ throw new Error(`Missing required secret: ${secret}`);
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### `list(scope?: SecretScope): Promise<SecretInfo[]>`
121
+
122
+ Lists all secrets in the specified scope with metadata (but not values).
123
+
124
+ ```typescript
125
+ // List all global secrets
126
+ const globalSecrets = await ctx.majk.secrets.list({ type: 'global' });
127
+ console.log('Global secrets:', globalSecrets.map(s => s.key));
128
+
129
+ // List project secrets
130
+ const projectSecrets = await ctx.majk.secrets.list({
131
+ type: 'project',
132
+ id: 'my-project'
133
+ });
134
+
135
+ // Check when secret was last used
136
+ projectSecrets.forEach(secret => {
137
+ console.log(`${secret.key} last used: ${secret.lastUsedAt || 'never'}`);
138
+ });
139
+ ```
140
+
141
+ ### `delete(key: string, scope?: SecretScope): Promise<boolean>`
142
+
143
+ Deletes a secret. Returns `true` if the secret was deleted, `false` if it didn't exist.
144
+
145
+ ```typescript
146
+ // Delete a specific secret
147
+ const wasDeleted = await ctx.majk.secrets.delete('old.apiKey', {
148
+ type: 'project',
149
+ id: 'legacy-project'
150
+ });
151
+
152
+ if (wasDeleted) {
153
+ console.log('Secret deleted successfully');
154
+ }
155
+ ```
156
+
157
+ ## Scoped APIs
158
+
159
+ For cleaner code when working with many secrets in the same scope, use the scoped APIs.
160
+
161
+ ### `forProject(projectId: string): ScopedSecretsAPI`
162
+
163
+ Returns a scoped API for project-specific secrets.
164
+
165
+ ```typescript
166
+ const projectSecrets = ctx.majk.secrets.forProject('ecommerce-app');
167
+
168
+ // All operations are automatically scoped to this project
169
+ await projectSecrets.set('stripe.publishableKey', 'pk_test_...');
170
+ await projectSecrets.set('stripe.secretKey', 'sk_test_...');
171
+
172
+ const publishableKey = await projectSecrets.get('stripe.publishableKey');
173
+
174
+ // List only returns keys (not full SecretInfo objects)
175
+ const allKeys = await projectSecrets.list();
176
+ console.log('Project secrets:', allKeys); // ['stripe.publishableKey', 'stripe.secretKey']
177
+ ```
178
+
179
+ ### `forIntegration(integrationId: string): ScopedSecretsAPI`
180
+
181
+ Returns a scoped API for integration-specific secrets.
182
+
183
+ ```typescript
184
+ const slackIntegration = ctx.majk.secrets.forIntegration('slack-workspace-123');
185
+
186
+ await slackIntegration.set('bot.token', 'xoxb-...');
187
+ await slackIntegration.set('signing.secret', 'abc123...');
188
+ await slackIntegration.set('webhook.url', 'https://hooks.slack.com/...');
189
+
190
+ // Check if integration is configured
191
+ if (await slackIntegration.has('bot.token')) {
192
+ const botToken = await slackIntegration.get('bot.token');
193
+ // Initialize Slack client
194
+ }
195
+ ```
196
+
197
+ ## Secret Scopes
198
+
199
+ ### Global Scope (`{ type: 'global' }`)
200
+ - Available across all projects and integrations
201
+ - Use for user-wide credentials (GitHub tokens, OpenAI keys)
202
+ - Highest level of access
203
+
204
+ ```typescript
205
+ // User's personal GitHub token
206
+ await ctx.majk.secrets.set('github.personalToken', 'ghp_...', { type: 'global' });
207
+
208
+ // OpenAI API key for all projects
209
+ await ctx.majk.secrets.set('openai.apiKey', 'sk-...', { type: 'global' });
210
+ ```
211
+
212
+ ### Project Scope (`{ type: 'project', id: 'project-id' }`)
213
+ - Isolated to specific project
214
+ - Use for project-specific credentials and configuration
215
+ - Perfect for environment variables and deployment secrets
216
+
217
+ ```typescript
218
+ const projectId = 'my-web-app';
219
+
220
+ // Database credentials for this project only
221
+ await ctx.majk.secrets.set('db.host', 'localhost', { type: 'project', id: projectId });
222
+ await ctx.majk.secrets.set('db.password', 'secret123', { type: 'project', id: projectId });
223
+
224
+ // API keys specific to this project's environment
225
+ await ctx.majk.secrets.set('stripe.secretKey', 'sk_test_...', { type: 'project', id: projectId });
226
+ ```
227
+
228
+ ### Integration Scope (`{ type: 'integration', id: 'integration-id' }`)
229
+ - Isolated to specific integration instance
230
+ - Use for third-party service connections
231
+ - Allows multiple instances of same integration type
232
+
233
+ ```typescript
234
+ // Slack workspace A
235
+ const slackA = ctx.majk.secrets.forIntegration('slack-workspace-a');
236
+ await slackA.set('bot.token', 'xoxb-111...');
237
+
238
+ // Slack workspace B
239
+ const slackB = ctx.majk.secrets.forIntegration('slack-workspace-b');
240
+ await slackB.set('bot.token', 'xoxb-222...');
241
+
242
+ // Discord server
243
+ const discord = ctx.majk.secrets.forIntegration('discord-server-123');
244
+ await discord.set('bot.token', 'ODc...');
245
+ ```
246
+
247
+ ## Common Patterns
248
+
249
+ ### Plugin Configuration with Secrets
250
+
251
+ ```typescript
252
+ // Check for required secrets during plugin initialization
253
+ async function validatePluginSecrets(): Promise<boolean> {
254
+ const requiredGlobalSecrets = ['openai.apiKey'];
255
+ const requiredProjectSecrets = ['database.url', 'api.secret'];
256
+
257
+ // Check global secrets
258
+ for (const key of requiredGlobalSecrets) {
259
+ if (!await ctx.majk.secrets.has(key, { type: 'global' })) {
260
+ console.error(`Missing required global secret: ${key}`);
261
+ return false;
262
+ }
263
+ }
264
+
265
+ // Check project secrets
266
+ const projectId = ctx.project?.id;
267
+ if (projectId) {
268
+ for (const key of requiredProjectSecrets) {
269
+ if (!await ctx.majk.secrets.has(key, { type: 'project', id: projectId })) {
270
+ console.error(`Missing required project secret: ${key}`);
271
+ return false;
272
+ }
273
+ }
274
+ }
275
+
276
+ return true;
277
+ }
278
+ ```
279
+
280
+ ### Secret Rotation
281
+
282
+ ```typescript
283
+ async function rotateApiKey(keyName: string, scope: SecretScope) {
284
+ // Generate new API key (implementation depends on service)
285
+ const newApiKey = await generateNewApiKey();
286
+
287
+ // Store the new key
288
+ await ctx.majk.secrets.set(keyName, newApiKey, scope);
289
+
290
+ // Optionally keep backup of old key temporarily
291
+ const oldKey = await ctx.majk.secrets.get(keyName, scope);
292
+ if (oldKey) {
293
+ await ctx.majk.secrets.set(`${keyName}.backup`, oldKey, scope);
294
+ }
295
+
296
+ console.log(`API key ${keyName} rotated successfully`);
297
+ }
298
+ ```
299
+
300
+ ### Environment-Specific Secrets
301
+
302
+ ```typescript
303
+ async function getEnvironmentSecret(key: string, environment: string, projectId: string) {
304
+ // Try environment-specific secret first
305
+ const envKey = `${environment}.${key}`;
306
+ let secret = await ctx.majk.secrets.get(envKey, { type: 'project', id: projectId });
307
+
308
+ // Fallback to general secret
309
+ if (!secret) {
310
+ secret = await ctx.majk.secrets.get(key, { type: 'project', id: projectId });
311
+ }
312
+
313
+ // Fallback to global secret
314
+ if (!secret) {
315
+ secret = await ctx.majk.secrets.get(key, { type: 'global' });
316
+ }
317
+
318
+ return secret;
319
+ }
320
+
321
+ // Usage
322
+ const apiKey = await getEnvironmentSecret('api.key', 'production', 'my-project');
323
+ ```
324
+
325
+ ### Bulk Secret Management
326
+
327
+ ```typescript
328
+ async function setupProjectSecrets(projectId: string, secrets: Record<string, string>) {
329
+ const projectSecrets = ctx.majk.secrets.forProject(projectId);
330
+
331
+ // Set all secrets
332
+ const operations = Object.entries(secrets).map(([key, value]) =>
333
+ projectSecrets.set(key, value)
334
+ );
335
+
336
+ await Promise.all(operations);
337
+ console.log(`Set ${operations.length} secrets for project ${projectId}`);
338
+ }
339
+
340
+ // Usage
341
+ await setupProjectSecrets('new-project', {
342
+ 'db.host': 'postgres.example.com',
343
+ 'db.user': 'app_user',
344
+ 'db.password': 'secure_password',
345
+ 'redis.url': 'redis://localhost:6379',
346
+ 'jwt.secret': 'super-secret-jwt-key'
347
+ });
348
+ ```
349
+
350
+ ## Security Best Practices
351
+
352
+ ### 1. Use Appropriate Scopes
353
+ - **Global**: Only for user-wide credentials (GitHub tokens, OpenAI keys)
354
+ - **Project**: For project-specific secrets (database passwords, API keys)
355
+ - **Integration**: For service-specific tokens and webhooks
356
+
357
+ ### 2. Secret Key Naming
358
+ Use clear, hierarchical naming:
359
+ ```typescript
360
+ // Good naming
361
+ 'aws.accessKey'
362
+ 'database.production.password'
363
+ 'slack.workspace-123.webhook.secret'
364
+ 'github.deployKey.private'
365
+
366
+ // Avoid vague names
367
+ 'key1'
368
+ 'secret'
369
+ 'token'
370
+ ```
371
+
372
+ ### 3. Error Handling
373
+ ```typescript
374
+ async function safeGetSecret(key: string, scope?: SecretScope): Promise<string> {
375
+ try {
376
+ const secret = await ctx.majk.secrets.get(key, scope);
377
+ if (!secret) {
378
+ throw new Error(`Secret '${key}' not found`);
379
+ }
380
+ return secret;
381
+ } catch (error) {
382
+ console.error(`Failed to retrieve secret '${key}':`, error);
383
+ throw error;
384
+ }
385
+ }
386
+ ```
387
+
388
+ ### 4. Secret Validation
389
+ ```typescript
390
+ async function validateSecretFormat(key: string, scope?: SecretScope): Promise<boolean> {
391
+ const secret = await ctx.majk.secrets.get(key, scope);
392
+ if (!secret) return false;
393
+
394
+ // Example: validate API key format
395
+ if (key.includes('openai') && !secret.startsWith('sk-')) {
396
+ console.warn(`Invalid OpenAI API key format for ${key}`);
397
+ return false;
398
+ }
399
+
400
+ return true;
401
+ }
402
+ ```
403
+
404
+ ## Important Notes
405
+
406
+ - **Never log secret values**: Always use `has()` for existence checks in logs
407
+ - **Secrets are encrypted**: Values are securely stored and encrypted at rest
408
+ - **Automatic cleanup**: Unused secrets are tracked via `lastUsedAt` timestamp
409
+ - **Case sensitive**: Secret keys are case-sensitive (`api.key` ≠ `API.KEY`)
410
+ - **No nested objects**: Store complex data as JSON strings if needed
411
+
412
+ The Secrets API ensures your sensitive data is handled securely while providing the flexibility to organize secrets by scope and context.