skills/rytass/utils/secret-development

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/utils
GitHub Stars
6
First Seen
Feb 5, 2026
Installed on
amp6
github-copilot6
replit6
codex6
kimi-cli6
gemini-cli6