Claude-skill-registry client-responsibilities
Client class responsibilities - connection ONLY, no business operations
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/client-responsibilities" ~/.claude/skills/majiayu000-claude-skill-registry-client-responsibilities && rm -rf "$T"
manifest:
skills/data/client-responsibilities/SKILL.mdsource content
Client Implementation Patterns
🚨 CRITICAL RULE #1: Client ONLY Handles Connection
- Client: ONLY connection management (connect, isConnected, disconnect)
- Producers: ALL API operations (list, get, create, update, delete)
- Client provides HTTP client instance to producers, nothing more
Client MUST NOT implement: list, get, create, update, delete, or any API operations.
Client Class - Connection Management Only
class ServiceClient { // ✅ Client responsibilities ONLY async connect(profile: ConnectionProfile): Promise<void> { // Setup authentication, HTTP client config } async isConnected(): Promise<boolean> { // Real API call to verify connection } async disconnect(): Promise<void> { // Cleanup } }
Connect Method Return Types
- If state needs persistence (tokens, expiration)Promise<ConnectionState>
- If no state persistence neededPromise<void>
ConnectionState Pattern
🚨 CRITICAL: ConnectionState Design Rules
- Include ALL refresh-relevant data - Store everything needed for token refresh
- MANDATORY: expiresIn field - All states MUST include
(extend baseConnectionState.yml)expiresIn- WHY: The server sets cronjobs based on
for automatic token refreshexpiresIn - UNIT: Must be in seconds (integer) until token expires
- REQUIRED: For any token that has an expiration
- WHY: The server sets cronjobs based on
- Store refresh tokens - If API provides refresh capability, store the refresh token
- Refresh method constraint -
can ONLY use ConnectionProfile + ConnectionState datarefresh() - expiresIn calculation from API responses:
- If API returns
(seconds) → Use directly asexpires_inexpiresIn - If API returns
(timestamp) → Calculateexpires_at
as seconds until that timeexpiresIn - If API returns other expiration format → Convert to
(seconds)expiresIn - DROP the original field - Only store
, notexpiresIn
or other formatsexpiresAt
- If API returns
What to Store in ConnectionState
MANDATORY when provided by API:
- Always store the current access tokenaccessToken
- Token expiration time (seconds or timestamp)expiresIn
- If API supports token refreshrefreshToken
- OAuth scope if relevant for refreshscope
- Type of token (bearer, etc.)tokenType
OPTIONAL based on API:
- If different endpoints for different tokensurl- Vendor-specific metadata needed for refresh
# ✅ CORRECT - Using core state (recommended) # connectionState.yml $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml' # Includes: tokenType, accessToken, refreshToken, expiresIn, scope, url # Already extends baseConnectionState.yml (which provides expiresIn)
# ✅ CORRECT - Custom state with all refresh data # connectionState.yml type: object allOf: - $ref: './node_modules/@zerobias-org/types-core/schema/baseConnectionState.yml' # Provides expiresIn - type: object required: - accessToken properties: accessToken: type: string format: password description: Current access token refreshToken: type: string format: password description: Token used to obtain new access token scope: type: string description: OAuth scope for this token # Note: expiresIn comes from baseConnectionState.yml
# ❌ WRONG - Missing refresh data and not extending baseConnectionState type: object properties: accessToken: type: string # Missing: expiresIn (MANDATORY - must extend baseConnectionState.yml) # Missing: refreshToken (needed for refresh capability)
Implementing connect() with State
// ✅ CORRECT - Store ALL relevant data from API (expiresIn provided directly) async connect(profile: ConnectionProfile): Promise<ConnectionState> { const response = await this.httpClient.post('/auth/login', { username: profile.username, password: profile.password }); // Store EVERYTHING the API provides that might be needed for refresh const state: ConnectionState = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, // Store for refresh() expiresIn: response.data.expires_in, // MANDATORY - seconds until expiration tokenType: response.data.token_type, // Store if needed for headers scope: response.data.scope // Store if needed for refresh }; this.connectionState = state; return state; // Framework persists }
// ✅ CORRECT - Calculate expiresIn when API returns expires_at (timestamp) async connect(profile: ConnectionProfile): Promise<ConnectionState> { const response = await this.httpClient.post('/auth/login', { username: profile.username, password: profile.password }); // Calculate expiresIn from expires_at timestamp const expiresAtTimestamp = new Date(response.data.expires_at).getTime(); const nowTimestamp = Date.now(); const expiresIn = Math.floor((expiresAtTimestamp - nowTimestamp) / 1000); // Convert to seconds // CRITICAL: Store ONLY expiresIn, DROP expires_at // The server needs expiresIn for cronjobs const state: ConnectionState = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, expiresIn: expiresIn, // MANDATORY - calculated from expires_at, in SECONDS tokenType: response.data.token_type, scope: response.data.scope // Note: expires_at NOT stored - only expiresIn is needed }; this.connectionState = state; return state; // Framework persists }
// ❌ WRONG - Storing expiresAt instead of expiresIn async connect(profile: ConnectionProfile): Promise<ConnectionState> { const response = await this.httpClient.post('/auth/login', { username: profile.username, password: profile.password }); const state: ConnectionState = { accessToken: response.data.access_token, expiresAt: response.data.expires_at, // ❌ WRONG - should be expiresIn (seconds) }; this.connectionState = state; return state; } // Problem: Server cannot set cronjob without expiresIn (seconds)
Implementing refresh() Method
CRITICAL:
refresh() can ONLY access:
- Original connection credentialsthis.connectionProfile
- Current state (with refreshToken, etc.)this.connectionState
// ✅ CORRECT - Uses only profile + state async refresh(): Promise<ConnectionState> { // Can use data from connectionState (refreshToken) const response = await this.httpClient.post('/auth/refresh', { refresh_token: this.connectionState.refreshToken, // Can also use profile data if needed client_id: this.connectionProfile.client_id }); // Update state with new tokens const newState: ConnectionState = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token || this.connectionState.refreshToken, expiresIn: response.data.expires_in, tokenType: response.data.token_type, scope: response.data.scope }; this.connectionState = newState; return newState; }
// ❌ WRONG - Requires data not in profile/state async refresh(): Promise<ConnectionState> { const response = await this.httpClient.post('/auth/refresh', { refresh_token: this.connectionState.refreshToken, device_id: 'hardcoded-value' // NO! Not in profile/state }); // This will fail - device_id should be in ConnectionProfile or ConnectionState }
When to use ConnectionState
Use ConnectionState (return from connect()):
- OAuth2 flows (access + refresh tokens)
- Session-based authentication
- APIs requiring token refresh
- Token expiration tracking needed
Use void (return from connect()):
- API key authentication (static, never expires)
- Basic auth (credentials used each request, no state)
- No refresh capability needed
Use Core Connection Profiles and States
🚨 CRITICAL RULE
- MANDATORY: Use existing core schemas from
when they match@zerobias-org/types-core/schema - FORBIDDEN: Creating custom connectionProfile.yml or connectionState.yml when core schema exists
Available Core Connection Profiles
# ✅ CORRECT - Token/API Key authentication # connectionProfile.yml $ref: './node_modules/@zerobias-org/types-core/schema/tokenProfile.yml' # Fields: apiToken (required), url (optional) # Use when: API uses a single token/key for authentication
# ✅ CORRECT - OAuth Client Credentials # connectionProfile.yml $ref: './node_modules/@zerobias-org/types-core/schema/oauthClientProfile.yml' # Fields: client_id (required), client_secret (required), url (optional) # Use when: OAuth client credentials grant (RFC 6749 section 4.4)
# ✅ CORRECT - OAuth Token-based # connectionProfile.yml $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenProfile.yml' # Fields: tokenType (default: bearer), accessToken (required), url (optional) # Use when: Pre-obtained OAuth token authentication
# ✅ CORRECT - Username/Password authentication (Basic Auth pattern) # connectionProfile.yml $ref: './node_modules/@zerobias-org/types-core/schema/basicConnection.yml' # Fields: uri (required, URL), username (required), password (required) # Use when: API uses username/password or email/password authentication # Note: For email specifically, you can extend this and change username to email with format: email
# ✅ CORRECT - Email/Password authentication (extending basicConnection) # connectionProfile.yml type: object allOf: - $ref: './node_modules/@zerobias-org/types-core/schema/basicConnection.yml' - type: object properties: username: type: string format: email # Override to require email format description: User email for authentication # Extends basicConnection but enforces email format on username field # Use when: API requires email specifically (not just any username)
Available Core Connection States
# ✅ CORRECT - Simple token state # connectionState.yml $ref: './node_modules/@zerobias-org/types-core/schema/tokenConnectionState.yml' # Fields: accessToken, expiresIn (from baseConnectionState) # Use when: Only need to persist access token with expiration # Note: Extends baseConnectionState.yml
# ✅ CORRECT - Full OAuth state # connectionState.yml $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml' # Fields: tokenType, accessToken, refreshToken, expiresIn (from base), scope, url # Use when: OAuth authorization code flow with refresh capability # Note: Extends baseConnectionState.yml
When to Create Custom Profile/State
Only create custom schemas when:
- Authentication method doesn't match any core profile
- Additional vendor-specific fields required beyond core profile fields
- Specialized authentication flow not covered by core
# ⚠️ CONSIDER FIRST - Can this use basicConnection.yml? # For username/password or email/password auth, prefer extending basicConnection.yml # See examples above for basicConnection.yml usage # ✅ ACCEPTABLE - Fully custom (but consider basicConnection first!) # connectionProfile.yml (custom when core types don't fit) type: object required: - email - password properties: email: type: string format: email password: type: string format: password baseUrl: type: string format: url default: https://api.vendor.com # Note: Could potentially extend basicConnection.yml instead
Decision Process
- Check if core profile matches authentication method
- Token/API Key →
tokenProfile.yml - OAuth client credentials →
oauthClientProfile.yml - OAuth token →
oauthTokenProfile.yml - Username/password or email/password →
(or extend it)basicConnection.yml
- Token/API Key →
- If exact match → Use core profile with $ref
- If partial match → Extend core profile (use allOf)
- If no match → Create custom profile with full schema (rare)
WHY: Core profiles ensure consistency, reduce duplication, and provide standard patterns that the framework expects.
Validation Scripts
Validate Client Implementation
# Check client only has connection methods grep -E "(async (list|get|create|update|delete|patch))" src/*Client.ts && echo "❌ Client has business logic!" || echo "✅ Client clean" # Check client implements required methods grep -E "(async connect|async isConnected|async disconnect)" src/*Client.ts && echo "✅ Client has required methods" || echo "❌ Missing client methods" # Check ConnectionState extends baseConnectionState or uses core state grep -E "(baseConnectionState\.yml|tokenConnectionState\.yml|oauthTokenState\.yml)" connectionState.yml && echo "✅ State extends base" || echo "⚠️ Check if expiresIn is defined"
Validate ConnectionState has expiresIn
# Check expiresIn is in state (either via base or custom) (grep -q "baseConnectionState.yml" connectionState.yml || grep -q "expiresIn" connectionState.yml) && echo "✅ expiresIn present" || echo "❌ Missing expiresIn!"
Validate Core Profile Usage
# Check if using core profiles grep -E "(tokenProfile\.yml|oauthClientProfile\.yml|oauthTokenProfile\.yml|basicConnection\.yml)" connectionProfile.yml && echo "✅ Using core profile" || echo "⚠️ Custom profile - verify it's necessary"