@saasifier/api-keys
v0.1.1
Published
Create, list, revoke, and verify scoped API keys for programmatic access to a tenant's resources.
Readme
@saasifier/api-keys
Create, list, revoke, and verify scoped API keys for programmatic access to a tenant's resources.
Installation
npm install @saasifier/api-keys
# or
pnpm add @saasifier/api-keysOverview
Many SaaS products need to let their own customers call back into the API programmatically — CI pipelines, integrations, server-to-server calls — without using interactive login. @saasifier/api-keys provides organization-scoped API keys with permission scopes, expiration, and revocation, built around a strict rule: raw key secrets are never persisted. Only a SHA-256 hash is stored, and the plaintext secret is returned to the caller exactly once, at creation time.
Usage
import { ApiKeyService } from "@saasifier/api-keys";
import type { DatabaseAdapter } from "@saasifier/database";
declare const database: DatabaseAdapter;
const apiKeys = new ApiKeyService(database);
// Create a key — the raw secret is only ever available here.
const { apiKey, secret } = await apiKeys.create("org_123", {
name: "CI deploy key",
scopes: ["deployments:write"],
expiresAt: new Date("2027-01-01")
});
console.log(`Save this now, it won't be shown again: ${secret}`);
// List active keys for an organization (no secrets included).
const keys = await apiKeys.list("org_123");
// Verify an incoming request's key and check its scope.
const verifiedKey = await apiKeys.verify(secret);
if (!apiKeys.hasScope(verifiedKey, "deployments:write")) {
throw new Error("Key is missing the required scope.");
}
// Revoke a key.
await apiKeys.revoke("org_123", apiKey.id);API Reference
ApiKeyService
saas.apiKeys.create/list/revoke, plus verify, used by whatever layer authenticates inbound API-key-bearing requests. Kept as its own service (rather than folded into @saasifier/core) so hashing/verification logic has a single, auditable home.
Constructor: new ApiKeyService(database: DatabaseAdapter)
create(organizationId: string, input: CreateApiKeyInput): Promise<CreatedApiKey>— generates a new secret, stores its hash, and returns both the persistedApiKeyrecord and the one-time rawsecret.list(organizationId: string): Promise<ApiKey[]>— lists an organization's API keys (never includes raw secrets).revoke(organizationId: string, apiKeyId: string): Promise<void>— revokes a key.verify(rawSecret: string): Promise<ApiKey>— hashes the given raw secret and looks up the matching key. ThrowsInvalidApiKeyErrorfor any of: unknown, revoked, or expired — deliberately without distinguishing which, so callers can't use the error to probe key state. Updates the key's last-used timestamp on success.hasScope(apiKey: ApiKey, scope: string): boolean— checks whether a verified key includes a given scope string.
CreateApiKeyInput
name: string— human-readable label for the key.scopes: string[]— permission scopes granted to the key.expiresAt?: Date— optional expiration.
CreatedApiKey
apiKey: ApiKey— the persisted record.secret: string— the raw secret, shown exactly once. It is never persisted or retrievable again.
Hashing utilities (hashing.ts)
Lower-level primitives ApiKeyService is built on, exported for advanced use:
generateApiKeySecret(): string— generates a new raw secret in the formsk_live_<48 hex chars>. Only this function ever produces plaintext; callers must display it once and never persist it.hashApiKeySecret(rawSecret: string): string— returns the SHA-256 hex digest of a raw secret; this is what gets persisted and compared against.lastFourOf(rawSecret: string): string— returns the last 4 characters of the raw secret, safe to store/display for key identification (e.g.sk_live_...ab12).
Key format, hashing, and verification flow
- Format: raw secrets look like
sk_live_<48 hex characters>(24 random bytes vianode:crypto'srandomBytes). - Storage: only
hashApiKeySecret(secret)(SHA-256 hex digest) andlastFourOf(secret)are persisted, viaDatabaseAdapter.apiKeys.create. The raw secret itself is never written to storage. - Verification:
ApiKeyService.verify(rawSecret)hashes the incoming secret and looks it up by hash viadatabase.apiKeys.findByHash. A key is rejected — with the same genericInvalidApiKeyError— if it doesn't exist, has been revoked (revokedAtset), or has expired (expiresAtin the past). A successful verification touches the key's last-used timestamp. - Scopes: each key carries a
scopes: string[]array set at creation. Callers are expected to checkhasScope(apiKey, requiredScope)after verification before allowing an action — scopes are opaque strings defined by the host application, not enforced by this package.
