Skip to content

ImSingee/jsonbin

Repository files navigation

jsonbin

A lightweight “JSON KV” service deployed on Cloudflare Workers:

  • Uses namespace as the top-level KV key, storing the entire JSON document in Cloudflare KV
  • Accesses/writes JSON subpaths via the URL path (a/b/c => JSONPath a.b.c)
  • Supports read-only / read-write authorization (including an admin master key)

Deployment

1) Prerequisites

  • Node.js (LTS recommended)
  • pnpm
  • A Cloudflare account with Workers enabled
  • Install and log in to wrangler

2) Install dependencies

pnpm install

3) Create Cloudflare KV (prod/preview) and bind it

This project binds a KV namespace via kv_namespaces in wrangler.toml:

kv_namespaces = [
  { binding = "JSONBIN", id = "...", preview_id = "..." }
]

If you deploy under your own account, create your own KV namespaces and write the id/preview_id back into wrangler.toml.

Example commands (follow the actual output from wrangler):

wrangler kv namespace create JSONBIN
wrangler kv namespace create JSONBIN --preview

Fill the output IDs into wrangler.toml:

  • id: production KV id
  • preview_id: preview KV id used by wrangler dev

4) Configure environment variables / secrets

The service uses the following configuration:

  • APIKEY (required, recommended): admin master key, used to mint tokens and for admin access
  • DEFAULT_AUTHORIZATION (optional): default authorization (applies when the requester is not admin)

Set APIKEY as a secret (so it won’t be committed into config files):

wrangler secret put APIKEY

DEFAULT_AUTHORIZATION can be placed under [vars] in wrangler.toml (an example is already provided).

The format of DEFAULT_AUTHORIZATION is a “querystring-like” key-value list:

  • key: an authorization entry (see “Authorization model”)
  • value: r or rw (empty string is treated as r)

For example:

[vars]
DEFAULT_AUTHORIZATION = "public::*=r&demo=rw"

5) Local development

pnpm dev

By default this runs wrangler dev src/index.ts.

6) Deploy to Cloudflare

pnpm deploy

Equivalent to: wrangler deploy --minify src/index.ts.


Authorization

1) Where the token comes from (user token)

Each request tries to read userToken from the following locations (priority high to low):

  1. URL query: ?key=...
  2. Header: Authorization: Bearer ...
  3. Cookie: key=...

See implementation in src/auth.ts#getUserToken().

2) Admin (admin / master key)

The requester is treated as admin if either condition is met:

  • Environment variable APIKEY is empty (not recommended)
  • userToken === APIKEY

Admins have read-write access to all paths, and can call admin APIs: /_/token, /_/list.

3) Default authorization (DEFAULT_AUTHORIZATION)

When the requester is not admin, the service reads DEFAULT_AUTHORIZATION as the “base permission”. If not configured, the default is no permission.

Examples:

  • demo=rw: read-write for the whole demo namespace
  • demo::a.b.c=rw: only allow accessing JSON subpath a.b.c under demo (and its children)

Note: the code uses URLSearchParams to parse, so DEFAULT_AUTHORIZATION is essentially a string like k=v&k2=v2.

4) JWT token (fine-grained authorization)

You can call /_/token with admin privilege to mint a JWT token with embedded permissions, then use it as userToken.

The service uses APIKEY as the JWT signing/verification key (see src/auth.ts#jwtSign / jwtVerify).

The a field in the JWT payload is the authorization table (Record<string,'r'|'rw'>). The service merges it with DEFAULT_AUTHORIZATION:

  • For the same entry, if either side is rw, the merged result is rw
  • Tokens that fail verification or don’t match the expected format are treated as invalid (401)

5) Matching rules for authorization entry keys

Authorization checks happen on namespace + '/' + jsonpath, where:

  • namespace: the first path segment (e.g. demo for /demo/a/b)
  • jsonpath: the remaining path segments joined by . (e.g. jsonpath is a.b for /demo/a/b)

Rules for entry keys:

  • If the key does not contain /: it’s treated as a namespace (e.g. demo)
    • It will be expanded to demo/ and demo/*
  • If the key contains /: it’s treated as namespace/jsonpath (e.g. demo/a.b)
    • If it does not end with *, children are also allowed (by appending .*)

Matching syntax uses micromatch (glob-style), so you can use wildcards like *.


API overview

Routes are defined in src/index.ts:

  • Public: GET /
  • Admin: GET /_/signin, GET /_/token, GET /_/list, GET /_/list/:prefix
  • KV data: GET|POST|PATCH|DELETE /* (note: currently DELETE effectively returns 405; see below)

All responses default to Content-Type: application/json; charset=utf-8, and CORS is enabled (via hono/cors in src/index.ts).

0) GET /

Health check + returns the authorization info parsed for the current request:

{ "ok": true, "authorization": "admin" }

When not admin, authorization may be null or an authorization table.

1) GET /_/signin

Writes the current request’s userToken into cookie key, then 302 redirects to /.

  • cookie: httpOnly, secure, sameSite=Strict, path=/, maxAge=180 days

Useful to “persist” ?key=... into a browser cookie.

2) GET /_/token (admin)

Mints a JWT token (signed with APIKEY) to carry fine-grained permissions.

Query parameters:

  • ttl: validity duration in seconds
    • omitted: defaults to 15 minutes
    • ttl>=0: exp = iat + ttl
    • ttl<0: do not set exp (no expiry)
  • svc: optional service name (written into payload as-is)
  • r: repeatable parameter, adds read-only entries (e.g. &r=demo&r=demo/a.b)
  • rw: repeatable parameter, adds read-write entries
  • raw=1: returns the raw token string (text/plain) instead of JSON

Response (JSON by default):

{
  "id": "...",
  "service": null,
  "createdAt": "2026-01-01T00:00:00.000Z",
  "expiresAt": "2026-01-01T00:15:00.000Z",
  "authorization": { "demo": "rw" },
  "token": "eyJ..."
}

If you provide no r/rw, the service will insert an example entry (test=r) to avoid minting an empty authorization table.

3) GET /_/list / GET /_/list/:prefix (admin)

Lists keys in KV (i.e. all namespaces).

  • GET /_/list: no prefix filter
  • GET /_/list/:prefix: filter by prefix
    • If :prefix ends with *, the trailing * will be removed before using it as the prefix

Query parameters:

  • limit: default 50; <=0 means no limit
  • cursor: pagination cursor

Response:

{
  "keys": ["demo", "public"],
  "next": "..." 
}

next being null means there are no more results.


KV data API (core)

Core idea:

  • The first path segment is the namespace
  • Each subsequent path segment is one JSON key (internally joined by . into jsonpath for authorization checks)

For example:

  • /demo => namespace demo, jsonpath []
  • /demo/a/b => namespace demo, jsonpath ["a","b"]

1) GET /:namespace/...

Reads data (requires r permission).

If the value read is a string, the service returns the raw string with text/plain; charset=utf-8; otherwise it returns JSON.

Supported query parameters (see src/kv_query.ts):

  • q: JSONPath query (via the jsonpath library)
  • first=1: only take the first element of the q result (requires the result to be an array)
  • shape: “project/reshape” the output; value is a JSON string
  • count=1: count arrays/objects (array length / object key count)

Examples:

# Read the whole JSON under demo
curl -H 'Authorization: Bearer <token>' https://<your-domain>/demo

# Read demo.a.b
curl -H 'Authorization: Bearer <token>' https://<your-domain>/demo/a/b

# JSONPath query + count
curl -H 'Authorization: Bearer <token>' 'https://<your-domain>/demo?q=$.items[*]&count=1'

2) POST /:namespace/...

Writes/overwrites the value at a subpath (requires rw permission).

  • Content-Type: application/json: request body is parsed as JSON
  • Content-Type: text/plain: request body is read as plain text and stored as a string

Example:

curl -X POST \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"a":1}' \
  https://<your-domain>/demo

3) PATCH /:namespace/...

Applies JSON Merge Patch for objects, or appends for arrays (requires rw permission).

Query parameters:

  • append=1: target value must be an array; the request body will be pushed as one element
  • max=N: only effective with append=1; when the array exceeds N, it is truncated to keep the last N elements
  • create=1: create the target path if it does not exist
    • with append=1, create an empty array []
    • otherwise create an empty object {}

Examples:

# Merge patch an object
curl -X PATCH \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true}' \
  https://<your-domain>/demo/config?create=1

# Append to an array
curl -X PATCH \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"ts": 123}' \
  'https://<your-domain>/demo/events?append=1&create=1&max=100'

4) DELETE /:namespace/...

Although the router registers DELETE (see src/index.ts), src/kv.ts currently does not implement a DELETE branch, so it returns 405 Method Not Allowed.

If you need to “delete a subpath” today, the available workaround is:

  • Use POST / PATCH to overwrite that path with null (semantically treated as delete)

Error responses

Non-2xx responses return JSON (see src/error.ts#errorToResponse):

{
  "ok": false,
  "error": "unauthorized",
  "message": "Authorization Key is Missing"
}

Common status codes:

  • 401: missing token or invalid token
  • 403: token provided but permission denied
  • 404: path not found
  • 405: method not allowed

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published