npm.io
2.0.0 • Published yesterdayCLI

zephr

Licence
MIT
Version
2.0.0
Deps
0
Size
94 kB
Vulns
0
Weekly
0
Stars
1

zephr

How agents hand off credentials, securely. CLI and JavaScript SDK for Zephr.

Create a one-time secret link: encrypted on your device and self-destructing after a single retrieval. Pass it between agents, services, and pipelines with no shared infrastructure and no plaintext on the server.

Designed for zero-knowledge secret handoff between independent systems: AI agents, CI/CD pipelines, GitHub Actions, and human operators.

New in v2.0: retrieveSecret() returns purgeAt as a Unix timestamp in seconds (breaking — it was a numeric string that never parsed as the documented ISO date). The plaintext ceiling rises to 14,336 UTF-8 bytes, and anonymous callers may now use the 5/15/30-minute expiries. See the CHANGELOG.

How it works

  1. A 256-bit key is generated locally. It never reaches Zephr's servers.
  2. Your secret is encrypted with AES-GCM on your device
  3. Only the ciphertext is uploaded to Zephr
  4. The link embeds the key in the URL fragment, which browsers never transmit to servers
  5. First retrieval atomically consumes the record. A second request returns 410.

Features

  • No shared infrastructure: a link is the entire transport mechanism between independent processes
  • Zero-knowledge: the server never receives your plaintext or encryption keys
  • Local encryption: AES-GCM-256 on your device before any network call
  • One-time access: record marked consumed atomically on first retrieval
  • Zero external dependencies: built on Node.js built-ins and Web Crypto only
  • Pipe support: reads from stdin
  • API key support for higher limits and longer expiry
  • Webhook callbacks: get notified when a secret is consumed (callbackUrl + callbackSecret)
  • Idempotency: auto-generated Idempotency-Key on every create for safe retries

Installation

# No installation required — one-off use, CI, agent environments
npx zephr "my-secret"

# Global install
npm install -g zephr

# SDK for Node.js 22+ and browser bundles
npm install zephr

CLI

zephr <secret> [options]              # Create a one-time secret
echo "secret" | zephr [options]       # Create from stdin
zephr retrieve <link> [options]       # Retrieve and decrypt

Create options:
  -e, --expiry <minutes> Expiration in minutes: 5, 15, 30, 60, 1440, 10080, or 43200 (default: 60; anonymous callers may use up to 60, every longer value requires an API key from any account)
  -s, --split            Return URL and key separately
  -H, --hint <label>     Plaintext label for routing and audit logs (1-128 printable ASCII, not encrypted)
  --callback-url <url>   HTTPS webhook URL — receive a signed event on consumption
  --callback-secret <s>  HMAC-SHA256 signing secret for the webhook (required with --callback-url)
  -k, --api-key <key>    API key; takes precedence over ZEPHR_API_KEY env var

Retrieve options:
      --url <url>        Secret URL (split mode)
      --key <key>        Encryption key (split mode)
  -k, --api-key <key>    API key

General:
  -v, --version          Show version
  -h, --help             Show help
Examples
# Pass a credential to a downstream process
echo "$DB_PASSWORD" | zephr --expiry 60

# Split mode: URL and key through separate channels
zephr "$API_KEY" --split

# Attach a plaintext label for routing and audit logs
zephr "$API_KEY" --hint STRIPE_KEY_PROD

# From password manager
pass show production/db | zephr

# From file
cat ~/.ssh/id_rsa.pub | zephr

# Authenticated: higher limits and longer expiry
zephr "$API_KEY" --expiry 10080 --api-key zeph_...
ZEPHR_API_KEY=zeph_... zephr "$API_KEY" --expiry 10080

# 30-day expiry (requires an API key)
zephr "$API_KEY" --expiry 43200 --api-key zeph_...

# Webhook callback — get notified when the secret is consumed
zephr "$API_KEY" --callback-url https://my-server.example.com/zephr-events \
  --callback-secret my-hmac-secret --api-key zeph_...
Idempotency

The CLI auto-generates an Idempotency-Key header on every create. If a request times out at the infrastructure level and is replayed, the server returns the cached response without creating a duplicate secret. Cache TTL: 24 hours.

Retrieve
# Standard mode — pass the full link
zephr retrieve "https://zephr.io/secret/abc123#v1.key..."

# Split mode — URL and key separately
zephr retrieve --url "https://zephr.io/secret/abc123" --key "v1.key..."

# From stdin (e.g. piped from another command)
echo "https://zephr.io/secret/abc123#v1.key..." | zephr retrieve

The decrypted secret is written to stdout (pipeable). If the secret has a hint, it is written to stderr:

Hint: STRIPE_KEY_PROD          ← stderr (metadata)
sk-live-abc123                 ← stdout (data)

This means zephr retrieve <link> | pbcopy copies only the plaintext.

Output

Standard mode:

[OK] Secret encrypted on your device
[OK] Uploaded to Zephr

Link: https://zephr.io/secret/abc123#v1.key...

Expires in 1 hour. One-time access only.

Split mode:

[OK] Secret encrypted on your device
[OK] Uploaded to Zephr

Split mode - share these separately:

URL: https://zephr.io/secret/abc123
Key: v1.key...

Expires in 1 hour. One-time access only.

Authentication

The CLI and SDK work without an account. No setup required. Higher quotas and expiries beyond 1 hour require a free account. Pass it via --api-key or the ZEPHR_API_KEY environment variable. Anonymous requests are capped at 3/day per IP with a 1 h max expiry.

Tier Create limit Expiry options Max size Authentication
Anonymous 3/day 5m, 15m, 30m, 1h 6 KB None
Free 500/month 5m, 15m, 30m, 1h, 24h, 7d, 30d 20 KB --api-key zeph_...

Getting an API key: Log in at zephr.io/account, open the API Keys tab, and create a key. The raw key is shown exactly once. Copy it immediately.

Passing the key:

# Flag takes precedence over the environment variable
zephr "secret" --api-key zeph_...

# Environment variable: preferred for CI and scripts
export ZEPHR_API_KEY=zeph_...
zephr "secret"

# Inline for one-off use
ZEPHR_API_KEY=zeph_... zephr "secret"

GitHub Actions: Add ZEPHR_API_KEY as a repository secret, then reference it in your workflow. All zephr steps are automatically authenticated:

env:
  ZEPHR_API_KEY: ${{ secrets.ZEPHR_API_KEY }}

steps:
  - run: echo "$SECRET" | zephr --expiry 60
    env:
      SECRET: ${{ secrets.MY_SECRET }}

The key is sent as Authorization: Bearer zeph_... on each request. An invalid or revoked key returns HTTP 401.

JavaScript / TypeScript SDK

Isomorphic: works in Node.js 22+ and any browser bundle. TypeScript declarations included. Zero external dependencies.

import { createSecret, retrieveSecret } from 'zephr';

// Named expiry constants for readability (raw integers also accepted)
import { EXPIRY } from 'zephr/limits.js';
// EXPIRY.MINUTES_5, EXPIRY.MINUTES_15, EXPIRY.MINUTES_30,
// EXPIRY.HOURS_1, EXPIRY.HOURS_24, EXPIRY.DAYS_7, EXPIRY.DAYS_30

Agent A encrypts and hands off the link. Agent B retrieves it exactly once:

// Agent A: encrypt and dispatch
const { fullLink } = await createSecret('sk-live-abc123', { expiry: 60 });
agentB.dispatch({ credential: fullLink });

// Agent B: consumed atomically on first read
const { plaintext } = await retrieveSecret(fullLink);

Split mode: URL and key through separate channels:

const { url, key } = await createSecret('db-password', { split: true, expiry: 60 });
agentB.dispatch({ credentialUrl: url });
sideChannel.send(key); // key never shares a channel with the URL

const { plaintext } = await retrieveSecret({ url, key });
Return value

createSecret() resolves to an object. Standard mode (split: false):

{
  mode:      'standard',
  fullLink:  'https://zephr.io/secret/Ht7kR2...#v1.key...',
  expiresAt: '2026-03-08T12:00:00.000Z',  // ISO 8601
  secretId:  'Ht7kR2...',                  // 22-char base64url ID
}

Split mode (split: true):

{
  mode:      'split',
  url:       'https://zephr.io/secret/Ht7kR2...',
  key:       'v1.key...',
  expiresAt: '2026-03-08T12:00:00.000Z',
  secretId:  'Ht7kR2...',
}

retrieveSecret() resolves to a RetrievalResult object with properties plaintext (string), hint (string or undefined), and purgeAt (number or undefined) — a Unix timestamp in seconds.

Authenticated use: pass your API key for higher limits and longer expiry:

// Free: 500/mo, 30d max expiry
const { fullLink } = await createSecret('sk-live-abc123', {
  expiry: 10080,                          // any value up to 43200 with an apiKey
  apiKey: process.env.ZEPHR_API_KEY,      // 'zeph_...'
});

// apiKey is optional on retrieve; include it to count against your authenticated quota
const { plaintext } = await retrieveSecret(fullLink, {
  apiKey: process.env.ZEPHR_API_KEY,
});

In GitHub Actions, expose the repository secret as an environment variable and process.env.ZEPHR_API_KEY is populated automatically. No code changes needed:

env:
  ZEPHR_API_KEY: ${{ secrets.ZEPHR_API_KEY }}
Webhook callback

Get notified when a secret is consumed — no polling needed:

const { fullLink } = await createSecret('db-password', {
  expiry: 60,
  hint: 'DB_PASSWORD_PROD',
  callbackUrl: 'https://my-orchestrator.example.com/zephr-events',
  callbackSecret: 'my-hmac-signing-secret',
  apiKey: process.env.ZEPHR_API_KEY,
});

When the secret is retrieved, Zephr POSTs a signed event:

{
  "event":       "secret.consumed",
  "event_id":    "550e8400-e29b-41d4-a716-446655440000",
  "secret_id":   "Ht7kR2mNqP3wXvYz8aB4cD",
  "occurred_at": "2026-03-22T14:32:00.000Z",
  "hint":        "DB_PASSWORD_PROD"
}

Verify the X-Zephr-Signature header — HMAC-SHA256 hex digest of the raw JSON body, signed with your callbackSecret. Use timing-safe comparison (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python). See examples/webhook-receiver for runnable Node.js and Python receivers.

Fire-and-forget in v1 — no retries. 5-second timeout. Redirects blocked.

Idempotency

The SDK auto-generates an Idempotency-Key on every create — retries are safe by default. If a request times out and the caller retries, the server returns the cached response without creating a duplicate.

Full SDK reference at zephr.io/docs.

Error handling

import { createSecret, retrieveSecret, ValidationError, EncryptionError, ApiError, NetworkError } from 'zephr';

try {
  const { fullLink } = await createSecret('my secret');
} catch (err) {
  if (err instanceof ValidationError) {
    // Invalid input: empty or whitespace-only string, exceeds 14,336 bytes, unsupported expiry
  } else if (err instanceof EncryptionError) {
    // AES-GCM key generation or encryption/decryption failed
  } else if (err instanceof ApiError) {
    console.error(err.statusCode);  // e.g. 429, 403, 401, 410
    console.error(err.code);        // e.g. 'MONTHLY_LIMIT_EXCEEDED'
  } else if (err instanceof NetworkError) {
    // Connection failed or timed out
  }
}

Common ApiError codes:

Code Status Meaning
INVALID_API_KEY 401 Key not found or revoked
UPGRADE_REQUIRED 403 Not permitted for the caller's tier (e.g. any expiry other than 60 min without an account, or a webhook callbackUrl without an account)
ANON_RATE_LIMIT_EXCEEDED 429 Anonymous daily limit reached (3/day per IP)
MONTHLY_LIMIT_EXCEEDED 429 Monthly create limit reached for this API key
PAYLOAD_TOO_LARGE 413 Encrypted blob exceeds the tier blob ceiling
SECRET_NOT_FOUND 404 Secret ID does not exist or has expired
SECRET_ALREADY_CONSUMED 410 Secret was already retrieved
SECRET_EXPIRED 410 Secret has passed its expiry time

Security

  • Encrypts on your device before any network call
  • AES-GCM-256 with authenticated encryption and built-in tamper detection
  • Keys never reach the server. They travel in the URL fragment (RFC 3986 §3.5), which browsers strip before sending requests.
  • No plaintext logging. No analytics in the CLI or SDK.

Requirements

Node.js 22.0.0 or higher

License

MIT

Keywords