@majkapp/plugin-kit 3.7.3 → 3.7.5
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/bin/promptable-cli.js +88 -0
- package/docs/AGENTS.md +641 -0
- package/docs/AUTH.md +248 -0
- package/docs/BATCH.md +256 -0
- package/docs/CONFIG_API.md +206 -0
- package/docs/CONVERSATIONS_API.md +283 -0
- package/docs/DELEGATION.md +196 -0
- package/docs/INDEX.md +38 -0
- package/docs/KNOWLEDGE.md +225 -0
- package/docs/PLUGINS.md +356 -0
- package/docs/REPORTS.md +271 -0
- package/docs/SCRIPTING.md +231 -0
- package/docs/SECRETS.md +412 -0
- package/docs/SKILLS.md +514 -0
- package/docs/TASKS.md +622 -0
- package/docs/TOOL_DISCOVERY.md +240 -0
- package/package.json +1 -1
package/docs/SECRETS.md
ADDED
|
@@ -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.
|