Development guide for the @rytass/secret base package. Use when creating new secret adapters, understanding base interfaces, or extending secret management functionality. Covers the SecretManager abstract class and common implementation patterns. Keywords: secret adapter, development guide, vault, credential, key management
本指南說明如何基於 @rytass/secret 基礎套件開發新的密鑰管理適配器。
@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
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;
}
// my-secret-adapter/src/typings.ts
export interface MySecretOptions {
endpoint: string;
apiKey: string;
namespace?: string;
cacheEnabled?: boolean;
cacheTTL?: number; // milliseconds
}
// 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();
}
}
// 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],
};
}
}
// 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);
}
}
根據後端特性選擇同步或非同步:
// 非同步(網路請求)
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
}
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);
}
}
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);
}
}
}
// __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);
});
});
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
SecretManager 抽象類別get, set, delete 方法@rytass/secret-adapter-* 命名規範Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer