How Apache Maka Manages API Keys and Secrets Locally: A Deep Dive into credential-store.ts and activation-secret-injector.ts

Apache Maka stores API keys and secrets in a permission-hardened JSON file managed by CredentialStore in credential-store.ts, while ActivationSecretInjector from activation-secret-injector.ts resolves and injects these secrets into sandboxed environments at runtime with automatic cleanup and redaction.

Apache Maka handles sensitive credentials through a two-layer architecture that separates persistent storage from runtime injection. The [credential-store.ts](https://github.com/apache/maka/blob/main/packages/storage/src/credential-store.ts) module provides durable, file-based storage with atomic writes and cross-process locking, while [activation-secret-injector.ts](https://github.com/apache/maka/blob/main/packages/storage/src/activation-secret-injector.ts) manages the secure resolution and injection of these secrets into isolated execution contexts. Together, these components ensure that API keys, OAuth tokens, and bot secrets remain protected at rest and are only exposed to specific processes that require them.

File-Based Secret Persistence in credential-store.ts

Maka persists secrets to a JSON file called credentials.json located at the workspace root. The createFileCredentialStore() factory function returns a FileCredentialStore instance that encapsulates all read and write operations, ensuring that concurrent processes cannot corrupt the underlying file through a combination of atomic writes and filesystem-level locking.

Atomic Writes and Cross-Process Locking

The store uses withCredentialFileLock (lines 23-42) to orchestrate exclusive access to the credentials file. This utility creates a temporary lock directory to guarantee that only one process can write at a time, preventing race conditions during parallel operations. Writes are performed atomically using writeSecretFileAtomic, which ensures that the existing file is never in a partially-written state.

Schema Validation and Filesystem Security

To prevent data corruption from version mismatches, the store enforces a strict CREDENTIAL_SCHEMA_VERSION (currently 1) that is validated on every read (lines 68-74). Unknown schema versions trigger a hard error, refusing to load potentially incompatible data. Filesystem permissions are hardened through ensureSecretDir and writeSecretFileAtomic: the JSON file receives 0600 permissions (owner read/write only), while the parent directory is set to 0700 (lines 44-57, 62-73).

Typed Storage and CRUD Operations

Public APIs use the CredentialKind enum (e.g., 'api_key', 'bot_token', 'app_secret') to categorize secrets. Internally, the toStoredKind() function maps these to concise storage keys like 'apiKey' and 'botAppSecret' (lines 57-66, 57-77). The getSecret, setSecret, and deleteSecret methods perform read-modify-write cycles under the file lock. When deleting, specifying a kind removes only the matching <slug>:<kind> entry, while omitting the kind wipes all secrets for that slug (lines 94-100, 38-48).

Compare-and-Set for Safe Concurrent Updates

The compareAndSetSecret method implements atomic compare-and-set (CAS) semantics (lines 69-86). It reads the current value, compares it to an expected baseline, and only commits the new value if they match. The operation returns whether the write succeeded and, if not, what the current value is. This mechanism enables safe token refreshes where multiple processes might attempt to update the same credential simultaneously.

Runtime Secret Injection via activation-secret-injector.ts

When functions or sandboxes require secret access, Maka uses the ActivationSecretInjector class to resolve references and inject values into isolated environments. Unlike direct file access, this approach validates integrity before injection and guarantees cleanup after execution.

Bulk Resolution of Secret References

The injector accepts an array of ActivationSecretEnvironmentBinding objects that reference stored secrets. It calls store.resolveForActivation(...) on the underlying ManagedSecretStore to fetch all required material in a single round-trip (lines 25-34). This batch resolution minimizes I/O overhead when activating contexts that require multiple API keys or tokens.

Pre-Injection Validation

Before injection, the resolved material undergoes strict validation through snapshotMaterial and sameReference checks (lines 35-51). The system verifies string lengths, reference integrity, and type correctness. Any mismatch throws a ManagedSecretError immediately, preventing partially-resolved injections that could leave an environment with incomplete credentials.

Sandboxed Environment Isolation

Secrets are injected via the ActivationSecretSink interface. The default implementation, ActivationEnvironmentSecretSink, writes values into a fresh process.env-like object rather than the host’s global process.env (lines 66-84, 88-95). This isolation ensures that secrets are only available to the specific sandboxed process and are not accidentally inherited by subsequent operations.

Leasing, Cleanup, and Redaction

The prepare() method returns an ActivationSecretInjectionLease that must be released after use. Calling release() triggers the sink to restore the original environment state and clears the injector’s internal #values cache (lines 35-45, 90-96). Additionally, PreparedInjectionHandle.redact() scrubs secret values from log messages using redactLiteralSecrets, replacing sensitive strings with [redacted] (lines 31-34, 12-20).

Implementation Examples

The following TypeScript examples demonstrate how to use the credential store and activation injector in practice.

Storing and Retrieving Secrets

Create a store instance rooted at your workspace directory to persist API keys:

const store = createFileCredentialStore('/path/to/workspace');

// Persist an API key for a service identified by its slug
await store.setSecret('my-service', 'api_key', 'sk-abcdef123456');

// Retrieve the stored key later
const apiKey = await store.getSecret('my-service', 'api_key');
console.log(apiKey); // 'sk-abcdef123456'

Safe Token Refresh with Compare-and-Set

Use compareAndSetSecret to handle concurrent updates safely during token refresh:

const result = await store.compareAndSetSecret(
  'my-service',
  'api_key',
  'sk-abcdef123456',      // expected current value
  'sk-newtoken987654',    // new value to write
);

if (!result.committed) {
  // Another process updated the key; current value is in result.currentValue
  console.log('Concurrent update detected, new value:', result.currentValue);
} else {
  console.log('Token refreshed successfully');
}

Injecting Secrets into Sandboxed Environments

Resolve and inject secrets for a specific activation context:

const injector = new ActivationSecretInjector(managedSecretStore);
const sandboxEnv = {}; // Fresh environment object

const prepared = await injector.prepare({
  context: activationContext,
  bindings: [
    {
      reference: { schemaVersion: 1, secretId: 'my-service:apiKey' },
      target: { kind: 'environment', name: 'MY_SERVICE_API_KEY' },
    },
  ],
  sink: new ActivationEnvironmentSecretSink(sandboxEnv),
});

// Secret is now available in the isolated environment
console.log(sandboxEnv.MY_SERVICE_API_KEY); // 'sk-abcdef123456'

// Cleanup when activation ends
await prepared.release();
// sandboxEnv.MY_SERVICE_API_KEY is now undefined

Summary

  • CredentialStore persists secrets to a JSON file at the workspace root with atomic writes, cross-process file locking, and strict schema versioning to prevent corruption.
  • Filesystem permissions are hardened to 0600 for the credentials file and 0700 for its parent directory, ensuring only the owner can access secret material at rest.
  • Compare-and-set operations enable safe concurrent updates, such as OAuth token refreshes, by only writing when the expected current value matches the stored value.
  • ActivationSecretInjector resolves secret references in bulk, validates integrity before injection, and isolates secrets in sandboxed environment objects rather than the global process environment.
  • Automatic cleanup via lease release restores the original environment state and clears internal caches, while redaction utilities prevent secret exposure in logs.

Frequently Asked Questions

How does Maka prevent concurrent processes from corrupting the credentials file?

Maka uses withCredentialFileLock (lines 23-42 in credential-store.ts) to implement cross-process mutual exclusion. This utility creates a temporary lock directory before any write operation, ensuring that only one process can modify the credentials.json file at a time. Combined with atomic write operations via writeSecretFileAtomic, this prevents partial writes and race conditions during concurrent access.

What security measures protect secrets at rest?

The credential store enforces 0600 file permissions (owner read/write only) on the credentials.json file and 0700 permissions on its parent directory (lines 44-57 and 62-73). Additionally, the store validates a CREDENTIAL_SCHEMA_VERSION on every read (lines 68-74), refusing to load files with unknown versions to prevent use of corrupted or outdated data.

How does the activation injector prevent secret leakage between sandboxed executions?

The ActivationSecretInjector uses the ActivationEnvironmentSecretSink to write secrets into a fresh process.env-like object rather than the host's global environment (lines 66-84 in activation-secret-injector.ts). When the lease is released, the sink restores the original environment state and the injector clears its internal #values cache (lines 90-96), ensuring secrets are not retained in memory or accessible to subsequent executions.

Can Maka handle concurrent token refreshes without race conditions?

Yes. The compareAndSetSecret method in credential-store.ts (lines 69-86) implements compare-and-set (CAS) semantics. It only writes the new value if the current stored value matches the expected baseline provided by the caller. If another process has updated the secret in the meantime, the operation returns the current value without overwriting, allowing the caller to retry with the updated baseline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →