GCP Secret Manager

Lightweight, dependency-free fake of Google Cloud Secret Manager that speaks the real Secret Manager v1 REST API (https://secretmanager.googleapis.com/v1), so application code using @google-cloud/secret-manager can run against it with zero cost and zero side effects.

KeyValue
Port4585
ProtocolSecret Manager v1 REST API (HTTP + JSON)
Compatible client@google-cloud/secret-manager (v6, google-gax v5)
Size~48 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Quick Start

Start the server:

import { GcpSecretmanagerServer } from "./services/gcp-secretmanager/src/server.js";

const server = new GcpSecretmanagerServer(4585);
await server.start();
// ... use it ...
await server.stop();

Connect with the real @google-cloud/secret-manager client. The fake speaks the HTTP/1.1 REST transport (the google-gax fallback mode), so the low-level gapic SecretManagerServiceClient must be constructed with fallback: true, protocol: "http", and an explicit apiEndpoint + port pointing at the fake:

import { v1 } from "@google-cloud/secret-manager";

const client = new v1.SecretManagerServiceClient({
  projectId: "parlel",
  fallback: true,          // use the HTTP/1.1 REST transport instead of gRPC
  protocol: "http",        // talk plain HTTP to the local fake
  apiEndpoint: "127.0.0.1",
  port: 4585,
  // Any credentials work — the fake never verifies them.
});

const parent = client.projectPath("parlel");

// Create a secret with automatic replication.
const [secret] = await client.createSecret({
  parent,
  secretId: "db-password",
  secret: { replication: { automatic: {} } },
});

// Add a secret version (the actual secret material).
await client.addSecretVersion({
  parent: secret.name,
  payload: { data: Buffer.from("s3cr3t-value", "utf8") },
});

// Access the latest version.
const [resp] = await client.accessSecretVersion({
  name: `${secret.name}/versions/latest`,
});
console.log(resp.payload.data.toString("utf8")); // "s3cr3t-value"

The high-level convenience client (SecretManagerServiceClient from the package root) works the same way — it is the same gapic client under the hood.

Implemented Operations

All 15 RPCs that the @google-cloud/secret-manager v1 client exposes are implemented. The library's listSecretsStream / listSecretsAsync / listSecretVersionsStream / listSecretVersionsAsync are client-side pagination variants of ListSecrets / ListSecretVersions and are exercised by the fake's pagination support.

Secrets

RPC / MethodHTTP routeNotes
createSecretPOST /v1/{parent=projects/*}/secrets?secretId=secretId is a query param; the Secret body holds replication, labels, etc. Defaults to automatic replication when omitted.
getSecretGET /v1/{name=projects/*/secrets/*}
listSecretsGET /v1/{parent=projects/*}/secretsSupports pageSize, pageToken, filter, and returns totalSize.
updateSecretPATCH /v1/{secret.name=projects/*/secrets/*}Requires an updateMask. Mutable fields: labels, annotations, topics, expireTime, ttl, rotation, versionAliases, versionDestroyTtl.
deleteSecretDELETE /v1/{name=projects/*/secrets/*}Optional etag precondition. Cascades to its versions and IAM policy.

Secret Versions

RPC / MethodHTTP routeNotes
addSecretVersionPOST /v1/{parent=projects/*/secrets/*}:addVersionAuto-assigns incrementing numeric version ids. Validates the optional payload.dataCrc32c integrity check.
getSecretVersionGET /v1/{name=projects/*/secrets/*/versions/*}Resolves the latest alias and any versionAliases entries.
listSecretVersionsGET /v1/{parent=projects/*/secrets/*}/versionsNewest-first. Supports pageSize, pageToken, filter, totalSize.
accessSecretVersionGET /v1/{name=projects/*/secrets/*/versions/*}:accessReturns base64 payload.data + payload.dataCrc32c. Fails on DISABLED / DESTROYED versions.
enableSecretVersionPOST /v1/{name=projects/*/secrets/*/versions/*}:enableOptional etag precondition.
disableSecretVersionPOST /v1/{name=projects/*/secrets/*/versions/*}:disableOptional etag precondition.
destroySecretVersionPOST /v1/{name=projects/*/secrets/*/versions/*}:destroyIrrecoverably erases the payload; sets destroyTime.

IAM

RPC / MethodHTTP routeNotes
getIamPolicyGET /v1/{resource=projects/*/secrets/*}:getIamPolicyReturns a default empty policy if none set.
setIamPolicyPOST /v1/{resource=projects/*/secrets/*}:setIamPolicyStores the policy verbatim and re-stamps the etag.
testIamPermissionsPOST /v1/{resource=projects/*/secrets/*}:testIamPermissionsGrants every requested permission.

Internal (parlel-only) endpoints

EndpointPurpose
GET /_parlel/healthHealth probe: { status, service, secrets, versions }.
POST /_parlel/resetWipe all in-memory state.
GET /_parlel/dumpDump every secret + version for debugging.

Regional bindings

Every RPC also has a regional additional_binding under projects/*/locations/*/.... The fake is region-agnostic: it normalizes the /locations/{loc} segment away, so a secret created globally is reachable via its regional name and vice-versa.

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
Create / Get / List / Update / Delete secrets✅ Supported
Add / Get / List / Access secret versions✅ Supported
Enable / Disable / Destroy versions✅ Supported
latest version alias + custom versionAliases✅ Supported
Automatic & user-managed replication (echoed)✅ Supported
Labels, annotations, topics✅ Supported
ttlexpireTime resolution✅ Supported
payload.dataCrc32c integrity validation (CRC32C)✅ Supported
clientSpecifiedPayloadChecksum flag✅ Supported
etag preconditions on delete / enable / disable / destroy✅ Supported
Server-side filter (name / labels / state)✅ Supported (approximate)
Pagination (pageSize, pageToken, totalSize)✅ Supported
IAM get / set / testPermissions✅ Supported
gRPC transport⚠️ Use fallback: true (REST). Native gRPC is intentionally not served.
Real CMEK encryption / KMS integration✓ By design — Plain in-memory storage — transport/at-rest crypto is unnecessary locally
Pub/Sub rotation notifications⟳ Roadmap — Fields stored, no messages published
Real IAM enforcement / authn⟳ Roadmap — Every caller is granted everything
Audit logging✓ By design — Not emulated

Error codes / shapes

Errors are returned as the standard Google google.rpc.Status JSON body, which the google-gax REST decoder transcodes back into the canonical gRPC status code on the client (error.code):

{
  "error": {
    "code": 404,
    "message": "Secret [projects/parlel/secrets/missing] not found.",
    "status": "NOT_FOUND"
  }
}
SituationgRPC codegRPC status
Secret / version / policy resource not found5NOT_FOUND
Invalid secret id, missing updateMask, immutable field, bad CRC32C3INVALID_ARGUMENT
Accessing a DISABLED / DESTROYED version, destroying twice, etag mismatch9FAILED_PRECONDITION
Creating a secret id that already exists9FAILED_PRECONDITION (non-retryable; see note below)
Unknown custom verb12UNIMPLEMENTED

Note on duplicate-create. The real service returns ALREADY_EXISTS (code 6). There is no HTTP status that the gax REST decoder maps back to code 6 (HTTP 409 decodes to ABORTED, which gax retries). To preserve the non-retryable, immediately-rejecting behavior callers expect, the fake surfaces duplicate-create conflicts as FAILED_PRECONDITION (code 9).

Running the tests

npx vitest run tests/gcp-secretmanager.test.ts

The suite starts the server on a high non-conflicting port, drives every operation through the real @google-cloud/secret-manager client over the REST transport, asserts the responses (happy paths + edge cases), and tears the server down in afterAll.

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

GOOGLE_CLOUD_PROJECT=parlel
GCLOUD_PROJECT=parlel
<!-- parlel:testenv:end -->