GuardID documentation

GuardID stamps an invisible, verifiable signature into PNG images. Use the @yavqolabs/guardid npm package to run it inside your own app — in Node or the browser — or call the HTTP API hosted on this site. Both share the same key and bit format, so anything protected one way verifies the other.

Install

npm install @yavqolabs/guardid

Zero runtime dependencies. Requires Node.js >= 18 (for the Web StreamsCompressionStream / DecompressionStream globals) or any modern browser. Ships as ESM with full TypeScript types.

The hosted API needs no install — see HTTP API.

npm package — @yavqolabs/guardid

Quick start

import { protect, verify } from "@yavqolabs/guardid";
import { readFile, writeFile } from "node:fs/promises";

// Stamp an image with a fresh, unique ID
const pngBytes = await readFile("photo.png");
const { png, id } = await protect(pngBytes);
await writeFile("photo.watermarked.png", png);
console.log(id); // "GuardID · 2026-11-01 · A7F3C2D1"

// Later, anywhere, with no secret of your own
const found = await verify(await readFile("photo.watermarked.png"));
// → "GuardID · 2026-11-01 · A7F3C2D1"  (or null if unprotected / tampered)

Inputs accept Uint8Array, ArrayBuffer, Blob (so browser File objects work directly), or an already-decoded Raster.

protect(input, options?)

Decodes the PNG, embeds the payload into the least-significant bits of every R/G/B channel, and re-encodes. The output is visually identical — max ±1/255 per channel.

OptionTypeDefaultDescription
options.idstringgeneratedCustom payload to stamp instead of generateWatermarkId().
options.keystringAPP_KEYScrambling key. Pass your own to create a private namespace invisible to the default detector.

Returns Promise<ProtectResult>:

FieldTypeDescription
pngUint8ArrayWatermarked image, PNG-encoded, ready to write.
idstringThe exact payload stamped into the image.
bitsUsednumberBits consumed by the signature (48 header + 8 per payload byte).
capacityBitsnumberTotal LSB capacity: width × height × 3.

verify(input, options?)

Reads the hidden bits, unscrambles them with the key and checks the GID1 magic header. Returns the stamped ID string, or null when the image carries no GuardID signature for that key — wrong key, re-encoding, or tampering all resolve to null rather than throwing.

// Browser: verify a File dropped by the user
const id = await verify(droppedFile);
if (id) console.log(`Genuine GuardID image: ${id}`);

Low-level API

ExportDescription
generateWatermarkId()Returns a fresh "GuardID · YYYY-MM-DD · XXXXXXXX" ID.
APP_KEYThe built-in signature key shared by the package, the API and the web studio.
embed(raster, payload, key)Bit-level embedding on a decoded Raster; returns new RGBA bytes plus usage stats.
extract(raster, key)Bit-level extraction; returns the payload string or null.
decodePng(bytes) / encodePng(raster)The zero-dependency PNG codec (8-bit, non-interlaced RGB/RGBA).
import { decodePng, encodePng, embed, extract, APP_KEY } from "@yavqolabs/guardid";

const raster  = await decodePng(pngBytes);
const stamped = embed(raster, "acme-studio · shot-042", APP_KEY);
const png     = await encodePng({ ...raster, rgba: stamped.rgba });

const back    = extract(await decodePng(png), APP_KEY); // "acme-studio · shot-042"

HTTP API

Hosted on this site under /api/v1. No authentication, no limits beyond size. Both endpoints accept the same two request shapes: raw PNG bytes, or JSON with a base64 / data-URL image.

POST/api/v1/watermark

Request — either form:

# raw PNG bytes
curl -X POST https://<your-deployment>/api/v1/watermark \
  -H "Content-Type: image/png" --data-binary @photo.png

# JSON with base64 (or a data: URL)
curl -X POST https://<your-deployment>/api/v1/watermark \
  -H "Content-Type: application/json" \
  -d '{ "image": "'"$(base64 -i photo.png)"'" }'

Response 200:

{
  "id": "GuardID · 2026-11-01 · A7F3C2D1",
  "png": "<base64 watermarked PNG>",
  "bitsUsed": 312,
  "capacityBits": 28800
}

POST/api/v1/verify

Same request shapes. Response 200:

{ "watermark": "GuardID · 2026-11-01 · A7F3C2D1", "protected": true }

An unprotected or altered image returns { "watermark": null, "protected": false }.

Errors

StatusWhen
400Invalid JSON, missing/empty image field, or bad base64.
413Decoded image larger than 25 MB.
415Unsupported Content-Type (send image/png or application/json).
422Not a PNG, or an unsupported PNG variant (palette, 16-bit, interlaced), or the payload exceeds image capacity.

GET/api/v1/watermark · /api/v1/verify

Both endpoints answer GET with a JSON usage summary.

Watermark format

The payload is framed as a 32-bit magic header ("GID1") + 16-bit payload length + UTF-8 payload bytes. That bit stream is XOR-scrambled with a deterministic keystream seeded from the key (xmur3 hash → mulberry32 PRNG), then written one bit into the LSB of each R, G and B channel in raster order. Alpha is never touched.

Capacity is width × height × 3 bits — a 1000×1000 image holds ~3M bits, roughly 375 KB of payload. Verification is all-or- nothing: without the key, or after any pixel damage to the first bytes, the magic header never matches and the watermark is undetectable.

Limits & best practices

Keep it PNG. Lossy re-encoding (JPEG, WebP-lossy) and resizing or cropping rewrite the LSBs and destroy the signature. If you need robustness against social-media recompression, that is frequency-domain territory — on the GuardID roadmap.

The default APP_KEY is public by design — any installable detector must be reproducible. It provides signature integrity (you can't forge it without the key), not secrecy. For a private namespace, pass your own key.

Supported inputs: 8-bit, non-interlaced RGB or RGBA PNGs. Grayscale, palette, 16-bit and Adam7 PNGs are rejected with a clear error — convert them first.