| name | sdk-key-management |
| description | Use when changing SDK key creation, hashing, display, revocation, public lookup, or related audit behavior. |
Generated from ai/registry.json. Do not edit manually.
Canonical skill: ../../../ai/skills/sdk-key-management.md.
Referenced context:
../../../ai/rules/sdk-key-rules.md
../../../ai/rules/api-tenant-access-rules.md
../../../ai/architecture/sdk-key-lifecycle.md
../../../ai/glossary/config-sdk-terms.md
../../../ai/examples/good-sdk-key-service.md
This file is compiled from canonical AI knowledge files. Edit canonical files under ai, then run npm run ai:sync.
Compiled AI Skill: sdk-key-management
Canonical Skill: ai/skills/sdk-key-management.md
SDK Key Management
Use this skill when changing SDK key creation, display, hashing, revocation, public lookup, or related audit behavior.
Goal
Preserve SDK keys as hashed read-only credentials scoped to one config and one environment.
Read First
ai/rules/sdk-key-rules.md
ai/rules/api-tenant-access-rules.md
ai/architecture/sdk-key-lifecycle.md
ai/glossary/config-sdk-terms.md
ai/examples/good-sdk-key-service.md
Workflow
- Require project admin access for private SDK key writes.
- Verify config and environment ownership before creating a key.
- Keep raw key generation and hashing in
sdk-key-crypto.
- Return raw keys only from creation responses.
- Audit lifecycle changes with
keyPrefix, not raw keys.
Expected Output
- Raw SDK keys are never stored, logged, or re-displayed.
- Public lookup remains hash-based and sessionless.
- Revoked keys cannot fetch public config.
Verification
- Run
npm --workspace @capture-flag/api run test after SDK key behavior changes.
- Run
npm --workspace @capture-flag/api run build after service or controller changes.
Referenced Context
Reference: ai/rules/sdk-key-rules.md
SDK Key Rules
Rules for SDK key creation, storage, display, revocation, and public lookup.
Always
- Generate raw SDK keys with
createRawSdkKey() and hash them with hashSdkKey() before persistence.
- Return the raw SDK key only immediately after creation.
- Persist only
keyHash and a display-safe keyPrefix.
- Require
project_admin for SDK key creation and revocation.
- Verify
configId and environmentId both belong to the target project before creating a key.
- Write audit logs for SDK key creation and revocation without storing the raw key.
- Treat SDK keys as read-only public config credentials scoped to one config and one environment.
Never
- Do not store, log, audit, or re-display the raw SDK key after creation.
- Do not use session auth for the public SDK config endpoint.
- Do not expose whether a missing or revoked SDK key ever existed.
- Do not create SDK keys for mismatched config/environment projects.
- Do not allow developer or viewer roles to create or revoke SDK keys unless role policy intentionally changes.
Reference: ai/rules/api-tenant-access-rules.md
API Tenant Access Rules
Rules for private API authorization and tenant isolation.
Always
- Use
SessionGuard and AuthenticatedRequest on session-only private API controllers.
- Use
AuthenticatedApiGuard plus API token tenant/scope guards on private management routes that also accept API tokens.
- Keep controllers thin: parse route params, pass
request.user.id, and delegate rules to services.
- Enforce tenant access through
AccessService before returning or mutating tenant-owned data.
- Resolve parent organization, project, config, environment, SDK key, or flag and verify it belongs to the current tenant path.
- Use
requireProjectAccess for project-scoped reads.
- Use
requireProjectRole for project-scoped writes with the narrowest allowed project roles.
- Use
requireOrganizationMember or requireOrganizationRole for organization-level reads and writes.
- Preserve the rule that organization
owner and admin roles can satisfy project access without explicit project membership.
- Preserve owner safety: organization admins cannot create, change, or remove organization owners, and organizations must keep at least one owner.
- Keep project
developer scoped to feature flag writes; do not allow it to manage segments, SDK keys, environments, configs, project members, or roles.
Never
- Do not trust global IDs alone.
- Do not leak cross-tenant data through
include or nested select shapes.
- Do not perform private API reads or writes before access checks.
- Do not expose whether a public SDK key ever existed when it is missing or revoked.
- Do not make public SDK endpoints depend on session auth; they authenticate through hashed SDK keys.
Required Domain Checks
configId and environmentId must belong to the same projectId before SDK keys, flag values, or config state changes.
- Project ownership must be validated before listing configs, environments, members, SDK keys, and feature flags.
- Organization ownership must be validated before adding organization or project members.
- Segment mutations require project administrative access because segments are reusable targeting assets shared by flag rules.
Reference: ai/architecture/sdk-key-lifecycle.md
SDK Key Lifecycle Architecture
SDK keys authenticate public config reads without session auth.
Creation
- Private API requires
project_admin through AccessService.requireProjectRole.
- Service verifies the selected config and environment belong to the same project.
createRawSdkKey() creates the only raw key shown to the user.
hashSdkKey() stores the lookup hash; keyPrefix is stored for display.
- Creation writes an audit log without storing the raw key.
Public Config Lookup
- The public SDK endpoint hashes the raw key from the route and looks up
sdk_keys.key_hash.
- Missing or revoked keys return not found without revealing existence.
- Valid keys scope the public config response to one config and one environment.
- Valid accesses update
lastUsedAt, including 304 Not Modified responses.
Revocation
Revocation requires project_admin, sets revokedAt, and writes an audit log. Revoked keys cannot fetch public config.
Reference: ai/glossary/config-sdk-terms.md
Config And SDK Terms
Terms shared by API, public config, evaluator, SDK, and client UI.
Feature Flag
SDK-visible setting controlled by Capture Flag. Supported types are boolean, string, integer, double, json_object, and json_array.
Default Value
Configured flag value stored in the database and emitted in Config JSON. This is not the same as SDK fallback value.
Fallback Value
Value supplied by application code when calling getValue. The SDK returns fallback when config is unavailable, invalid, missing, or type-mismatched.
Rules
Ordered targeting rules emitted as rules in public Config JSON. The evaluator checks rules before percentage rollout.
Segment
Reusable group of attribute conditions scoped to one config and emitted as segments in public Config JSON.
Segment Reference
A rule condition shaped as { "segment": "segment-key" }. It is evaluated locally by checking the referenced segment conditions against the Evaluation Context.
Prerequisite Flag
A rule condition shaped as { "prerequisiteFlag": "flag-key", "operator": "equals", "value": true }. It evaluates another flag from the same Config JSON locally before deciding whether the current rule matches.
Advanced Targeting
Targeting features beyond simple attribute equality: prerequisite flags, arrayContains, date comparisons, and SemVer comparisons.
Percentage Rollout
Deterministic distribution of values based on a rollout attribute such as identifier.
Percentage Attribute
Context attribute used for rollout bucketing. Defaults to identifier.
SDK Key
Read-only credential scoped to one config and one environment. Stored as a hash in the API database.
Raw SDK Key
The full SDK credential shown only immediately after creation. It must never be stored, logged, audited, or re-displayed.
Key Prefix
Display-safe prefix derived from the raw SDK key and stored for UI identification and audit metadata.
Config JSON
Versioned public JSON downloaded by SDKs. It contains local-evaluation data, not evaluated flag results.
React SDK
React provider and hook package that receives a JavaScript SDK client and evaluates flags through that client.
Config Environment State
Per config + environment state that stores revision, ETag, and generated timestamp for public config caching.
ETag
HTTP cache validator used by SDK clients through If-None-Match.
Lazy Loading
Default SDK mode. The client fetches config only when no cache exists or when the cached entry is older than cacheTtlMs.
Auto Polling
SDK mode where the JavaScript client refreshes config in the background on pollIntervalMs.
Manual Refresh
SDK mode where getValue uses the current cache and the application calls refresh() to fetch new config.
Offline Mode
SDK mode where the client uses only existing cache and never performs network requests.
Memory Cache
Default in-process SDK cache used by every client instance.
localStorage Cache
Browser persistent cache enabled explicitly through SDK options. It stores config, ETag, timestamp, cache schema version, and scope fingerprints, never raw SDK keys or raw base URLs.
Cache TTL
Duration used by lazy loading to decide whether a cached config should be refreshed.
Cached ETag
ETag stored with the cached config and sent on refresh through If-None-Match.
Client Close
client.close() stops SDK-owned background polling timers.
Reference: ai/examples/good-sdk-key-service.md
Good SDK Key Service
Source: apps/api/src/sdk-keys/use-cases/create-sdk-key.service.ts (sha256: e2c83a6bdf1653fc73daa5a2e07e2ddb2efcd2bd01b8f4e8e2249eb222bcdc51)
Source: apps/api/src/sdk-keys/use-cases/revoke-sdk-key.service.ts (sha256: b1e6316e25347dfbd6fd6c7a3b4431361ca11c2eb206e3c18d1cbadd422576f8)
Source: apps/api/src/sdk-keys/use-cases/rotate-sdk-key.service.ts (sha256: b88c81c89b3ef70f5cb8ab730ae23982dfd5520f24c7c91cd9c07d988fa1625a)
Source: apps/api/src/common/sdk-key-crypto.ts (sha256: 9e1b5884fe94d12dd4004e39e0aa9a90328e1351a79215c1b8c53b81293c5a04)
Why this is canonical:
- Separates raw SDK key display from hashed persistence.
- Requires project admin access before key creation/revocation.
- Audits SDK key lifecycle without storing the raw credential.
- Rejects duplicate revocation attempts to avoid audit churn.
Raw Key And Hash
const credential = this.sdkKeyCredential.createCredential();
const sdkKey = await tx.sdkKey.create({
data: {
projectId,
configId: config.id,
environmentId: environment.id,
name: input.name?.trim() || `${config.name} ${environment.name} SDK Key`,
keyPrefix: credential.keyPrefix,
keyHash: credential.keyHash,
},
});
Only keyHash and keyPrefix are persisted.
Creation Return Shape
return {
...sdkKey,
key: credential.rawKey,
};
The raw key is returned only immediately after creation.