secret-development

Secret Adapter Development Guide (密鑰 Adapter 開發指南)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "secret-development" with this command: npx skills add rytass/utils/rytass-utils-secret-development

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 &#x26;&#x26; 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-* 命名規範

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

logistics-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

storage-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

invoice-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

sms-development

No summary provided by upstream source.

Repository SourceNeeds Review