secret-development
SKILL.md
Secret Adapter Development Guide (密鑰 Adapter 開發指南)
Overview
本指南說明如何基於 @rytass/secret 基礎套件開發新的密鑰管理適配器。
Base Package Architecture
@rytass/secret (Base)
└── SecretManager (Abstract Class)
├── project: string
├── get<T>(key): Promise<T> | T
├── set<T>(key, value): Promise<void> | void
└── delete(key): Promise<void> | void
Core Abstract Class
SecretManager
abstract class SecretManager {
constructor(project: string);
get project(): string;
abstract get<T>(key: string): Promise<T> | T;
abstract set<T>(key: string, value: T): Promise<void> | void;
abstract delete(key: string): Promise<void> | void;
}
Implementing a New Adapter
Step 1: Define Configuration Interface
// my-secret-adapter/src/typings.ts
export interface MySecretOptions {
endpoint: string;
apiKey: string;
namespace?: string;
cacheEnabled?: boolean;
cacheTTL?: number; // milliseconds
}
Step 2: Implement Secret Manager
// my-secret-adapter/src/my-secret.ts
import { SecretManager } from '@rytass/secret';
import axios from 'axios';
export class MySecret extends SecretManager {
private cache: Map<string, { value: unknown; expiry: number }> = new Map();
constructor(
project: string,
private readonly options: MySecretOptions,
) {
super(project);
}
async get<T>(key: string): Promise<T> {
// Check cache first
if (this.options.cacheEnabled) {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.value as T;
}
}
const response = await axios.get(
`${this.options.endpoint}/secrets/${this.project}/${key}`,
{
headers: { 'Authorization': `Bearer ${this.options.apiKey}` },
},
);
const value = response.data.value as T;
// Update cache
if (this.options.cacheEnabled && this.options.cacheTTL) {
this.cache.set(key, {
value,
expiry: Date.now() + this.options.cacheTTL,
});
}
return value;
}
async set<T>(key: string, value: T): Promise<void> {
await axios.put(
`${this.options.endpoint}/secrets/${this.project}/${key}`,
{ value },
{
headers: { 'Authorization': `Bearer ${this.options.apiKey}` },
},
);
// Invalidate cache
this.cache.delete(key);
}
async delete(key: string): Promise<void> {
await axios.delete(
`${this.options.endpoint}/secrets/${this.project}/${key}`,
{
headers: { 'Authorization': `Bearer ${this.options.apiKey}` },
},
);
// Invalidate cache
this.cache.delete(key);
}
// Optional: Clear all cache
clearCache(): void {
this.cache.clear();
}
}
Step 3: Add NestJS Module (Optional)
// my-secret-adapter-nestjs/src/my-secret.module.ts
import { Module, DynamicModule, Global } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MySecret, MySecretOptions } from '@rytass/my-secret-adapter';
export const MY_SECRET_SERVICE = Symbol('MY_SECRET_SERVICE');
@Global()
@Module({})
export class MySecretModule {
static forRoot(options: MySecretOptions & { project: string }): DynamicModule {
return {
module: MySecretModule,
providers: [
{
provide: MY_SECRET_SERVICE,
useFactory: () => new MySecret(options.project, options),
},
],
exports: [MY_SECRET_SERVICE],
};
}
static forRootAsync(options: {
useFactory: (...args: any[]) => MySecretOptions & { project: string };
inject?: any[];
}): DynamicModule {
return {
module: MySecretModule,
providers: [
{
provide: MY_SECRET_SERVICE,
useFactory: (...args) => {
const config = options.useFactory(...args);
return new MySecret(config.project, config);
},
inject: options.inject || [],
},
],
exports: [MY_SECRET_SERVICE],
};
}
}
Step 4: Create NestJS Service Wrapper
// my-secret-adapter-nestjs/src/my-secret.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { MySecret } from '@rytass/my-secret-adapter';
import { MY_SECRET_SERVICE } from './my-secret.module';
@Injectable()
export class MySecretService {
constructor(
@Inject(MY_SECRET_SERVICE)
private readonly manager: MySecret,
) {}
async get<T = string>(key: string): Promise<T> {
return this.manager.get<T>(key);
}
async set<T = string>(key: string, value: T): Promise<void> {
return this.manager.set(key, value);
}
async delete(key: string): Promise<void> {
return this.manager.delete(key);
}
}
Design Patterns
Sync vs Async Operations
根據後端特性選擇同步或非同步:
// 非同步(網路請求)
async get<T>(key: string): Promise<T> {
return await fetchFromRemote(key);
}
// 同步(本地快取)
get<T>(key: string): T {
return this.localCache.get(key);
}
// 混合模式(像 Vault adapter)
get<T>(key: string): VaultGetType<Options, T> {
if (this.options.online) {
return this.fetchOnline(key); // Promise<T>
}
return this.localCache.get(key); // T
}
Event-Driven Architecture
import { EventEmitter } from 'events';
export enum MySecretEvents {
CONNECTED = 'CONNECTED',
DISCONNECTED = 'DISCONNECTED',
ERROR = 'ERROR',
}
export class MySecret extends SecretManager {
private emitter = new EventEmitter();
on(event: MySecretEvents, listener: (...args: any[]) => void): void {
this.emitter.on(event, listener);
}
private emit(event: MySecretEvents, ...args: any[]): void {
this.emitter.emit(event, ...args);
}
}
Fallback Mechanism
export class MySecretWithFallback extends SecretManager {
constructor(
project: string,
private readonly primary: SecretManager,
private readonly fallback: SecretManager,
) {
super(project);
}
async get<T>(key: string): Promise<T> {
try {
return await this.primary.get<T>(key);
} catch {
return await this.fallback.get<T>(key);
}
}
}
Testing Guidelines
// __tests__/my-secret.spec.ts
import { MySecret } from '../src';
describe('MySecret', () => {
const secret = new MySecret('test-project', {
endpoint: 'https://api.example.com',
apiKey: 'test-key',
cacheEnabled: true,
cacheTTL: 5000,
});
it('should get secret value', async () => {
const value = await secret.get<string>('DATABASE_URL');
expect(value).toBeDefined();
});
it('should set and get secret', async () => {
await secret.set('TEST_KEY', 'test-value');
const value = await secret.get<string>('TEST_KEY');
expect(value).toBe('test-value');
});
it('should delete secret', async () => {
await secret.set('TO_DELETE', 'value');
await secret.delete('TO_DELETE');
await expect(secret.get('TO_DELETE')).rejects.toThrow();
});
it('should use cache', async () => {
const spy = jest.spyOn(axios, 'get');
await secret.get('CACHED_KEY');
await secret.get('CACHED_KEY');
expect(spy).toHaveBeenCalledTimes(1);
});
});
Package Structure
my-secret-adapter/
├── src/
│ ├── index.ts
│ ├── typings.ts
│ └── my-secret.ts
├── __tests__/
│ └── my-secret.spec.ts
├── package.json
└── tsconfig.build.json
my-secret-adapter-nestjs/ # Optional NestJS wrapper
├── src/
│ ├── index.ts
│ ├── my-secret.module.ts
│ └── my-secret.service.ts
└── package.json
Publishing Checklist
- 繼承
SecretManager抽象類別 - 實現
get,set,delete方法 - 定義清楚的配置介面
- 實現快取機制(如適用)
- 實現錯誤處理
- 撰寫單元測試
- 提供 NestJS 模組包裝(可選)
- 遵循
@rytass/secret-adapter-*命名規範
Weekly Installs
6
Repository
rytass/utilsGitHub Stars
6
First Seen
Feb 5, 2026
Security Audits
Installed on
amp6
github-copilot6
replit6
codex6
kimi-cli6
gemini-cli6