Archnemix

Image Protection · Open Source · 0BSD

Documentation · v1.30.3

Use it, step by step.

Everything you need to protect images with Archnemix — install, a five-minute quick start, the three profiles, the options that matter, the server rules that keep masters private, and an honest account of what the tool cannot do. The full reference manual lives on GitHub; this page is the guided path through it.

Install

Archnemix is one npm package. The browser runtime has no dependencies; the optional Node server uses Sharp for image processing.

npm install @archnemix/image-protection

Package entry points, so you import only what you run:

ImportWhat it is
@archnemix/image-protectionThe browser viewer and loader (Archx.load.image(…)).
…/serverThe Node middleware that authorizes viewers and serves one-time derivatives.
…/scrambleThe environment-agnostic scramble/encrypt engine used by both sides.
…/redisShared state-store adapter for multi-instance deployments.

Prefer no bundler? The package ships a prebuilt browser bundle at dist/archx.min.js that you can serve with an SRI pin — the exact integrity hash is printed in the project README so a CDN can never silently swap the code.

Quick start: browser only

Static sites and public previews can scramble assets at build time and unscramble them in the visitor’s browser — no server component required. This adds friction to casual saving, but it does not stop a determined scraper or make the asset private: the derivative and the browser-visible key are public by design. Keep licensed/private masters on the server path below.

import Archx from '@archnemix/image-protection';

// One image
Archx.load.image('#hero-photo', '/protected/aurora.archx', {
  profile: 'performance',
  key: window.__ARCHX_STATIC_KEY__,
});

// A whole gallery (lazy: loads as each image scrolls into view)
Archx.load.gallery('#gallery', [
  '/protected/photo-a.archx',
  '/protected/photo-b.archx',
], { profile: 'performance', key: window.__ARCHX_STATIC_KEY__ });

Create the .archx payloads ahead of time with the CLI, using the same key in the browser example above:

export ARCHX_SCRAMBLE_KEY='<random key>'
npx archx-protect photo.jpg photo.archx --grid 16
# or a full folder of responsive derivatives + manifest:
npx archx-build ./masters ./protected

Inject that key into window.__ARCHX_STATIC_KEY__ for the browser. Anyone who can view the page can obtain it, so static scrambling is obfuscation for public previews, not access control.

Honest note: there is no live demo on this site. A static page cannot authorize viewers or keep masters private, so the interactive scenes here are labelled illustrations. To see the real protected viewer, run the demo from the repository (npm run demo).

Quick start: with the server

The server is where the real boundary lives: masters stay outside your public folder, your own code decides who may see what, and each delivery is a short-lived, one-time token.

import express from 'express';
import { createArchxMiddleware } from '@archnemix/image-protection/server';

const app = express();
app.use(createArchxMiddleware({
  imagesDir: '/private/masters',        // outside public/, always
  publicDirs: ['/var/www/public'],      // declare every static root to check overlap
  maxWidthFraction: 0.6,                 // preview-only cap vs. the master's width
  secret: process.env.ARCHX_SECRET,     // 32+ random chars
  authorize: async (req, image) =>
    req.user ? { maxWidth: 1600 } : false,   // deny by default
}));
// In the page: use the protected server endpoint and an image id.
Archx.load.image('#hero-photo', { id: 'aurora-valley' }, {
  tokenEndpoint: '/archx/token/',
  profile: 'elite',
});

The browser asks the middleware for a token, authorize runs (your session/licence logic), and the authorized derivative is delivered once as scrambled tiles that the guarded viewer reassembles. The server does not guarantee preview-only pixels by default: maxWidthFraction defaults to 1, so native-width pixels may be delivered when other caps allow. Set it below 1 (for example, 0.6) when an asset class must never reach its master’s native width. Denied requests get nothing; replay is denied while the one-time spend record is retained.

Profiles: lite · performance · elite

Three canonical profiles set sensible defaults in one word. Any individual option you set afterwards still wins — a profile only fills in what you did not specify.

ProfileOptimizes forTypical use
liteLeast CPU, memory and bytes. Canvas renderer, smaller cache, forensic marking off.Catalogues, thumbnails, low-end phones, thousands of images per page.
performanceThe balance: strong protection without starving the device. Forensic marking on.Most galleries, shops and portfolios. Start here.
eliteMaximum hardening: WebGL vault, proof-of-possession required, 60 Hz temporal interlace, dynamic shields.Paid previews, unreleased work, high-value originals.

On the server, the matching helpers (withLiteDefaults, withPerformanceDefaults, withEliteDefaults) converge on the same three profiles. Older mode names (catalog, vault, fortress…) keep working, and fully custom flag combinations always remain available — profiles are defaults, not cages. You can also set your own page-wide defaults once:

Archx.config({ profile: 'performance', watermark: '@your-studio' });

Key options

The options you will actually reach for. The complete list (80+ client options, all documented) is in the README’s option reference.

OptionDefaultWhat it does
profile—'lite' | 'performance' | 'elite' — fills unset options from the profile above.
lazytrueLoad only when scrolled near the viewport (tune with rootMargin).
tokenEndpointnullRequired for { id } sources; set it to your middleware route (for example, '/archx/token/') so the viewer can mint a one-time delivery token.
watermarknullVisible watermark text (or an object for position/opacity).
forensictrueInvisible per-viewer forensic marking layer (the lite profile turns it off).
temporalInterlacenullSplits the picture across alternating 60 Hz frames so captures hold only a subset + fill. Elite sets it on.
proofOfPossession'auto'Binds the token to a non-extractable browser key so a copied URL cannot be redeemed elsewhere.
guardsmost on; DevTools offRight-click, drag, copy, long-press, print, blur and screenshot-signal guards are enabled by default; the DevTools-size heuristic is opt-in. Each is independently configurable.
onAttempt—Callback/DOM events when copy attempts are detected, for your own logging.

Client options are not a strict unknown-key allowlist: unsupported keys may appear in the returned config or instance-options view without enabling a corresponding Archx feature. Some invalid enum values normalize to a fallback instead of throwing; unknown profile names do throw. Double-check option spelling and verify the behavior you intend. Server middleware configuration is stricter and rejects unknown option keys at startup.

Galleries & many images

Archnemix is built for image-heavy pages. Images load lazily as they approach the viewport, tokens for visible images are minted in one batch request, decoded bitmaps live in a bounded LRU cache, and off-screen images can unload to give memory back. A page can carry hundreds of protected images without hundreds of token round-trips:

const gallery = Archx.load.gallery('#gallery', [
  { id: 'photo-1' }, { id: 'photo-2' },
], { tokenEndpoint: '/archx/token/', profile: 'lite' });
await gallery.ready;   // allSettled: each item can succeed or fail independently

On the server, identical derivative requests coalesce (one Sharp job serves concurrent viewers), processed derivatives are reused from a bounded cache, and handler.stats() shows hit rates so you can size things with data.

Events & offline helpers

The package root provides named event/plugin APIs (onClientEvent, offClientEvent, usePlugin, and listPlugins) as well as the convenient Archx.on/off/use/plugins methods. Global events are frozen and sanitized: no tokens, keys, pixels, source objects, or raw errors. Plugin setup receives only the registration API/version; an event listener can also receive a ProtectedImage instance, which should not be forwarded to telemetry.

For ingest/offline workflows, the root and /scramble entry expose assayStegoEmbed (real embed-quality/content-class pre-flight) and applyForensicBlocks (mutating RGBA pixel helper). The assay measures your chosen pixels and can optionally self-detect; it is not a detection guarantee or standalone attribution. The README documents signatures, return fields, and mutation behavior.

The five server rules

Most real-world failures of tools like this are deployment failures. These five rules are enforced or checked by the library — follow them and the system does what the overview promises:

  1. Keep masters outside every public root. If the original file is web-addressable, nothing else matters. imagesDir must not be inside a static mount. Declare each public root with publicDir/publicDirs so Archx can check for overlap; startup cannot detect undeclared host mounts, which remain your responsibility.
  2. authorize is required unless public access is explicit. Omitting both authorize and allowAnonymous:true fails at startup. The latter is an intentional public-catalog exception, not a safe default.
  3. Use a real secret. 32+ random characters from an environment variable or secret manager; the startup audit flags weak, shared, or well-known values.
  4. Cap the resolution. The default maxWidthFraction is 1, so an entitled viewer may receive native-width pixels when other caps permit. Set it below 1 (for example, 0.6) to make “preview only” a server-enforced property, not a client suggestion.
  5. Run the production audit in CI. auditArchxProductionConfig(options) returns blockers (public masters, query-string tokens, single-instance state behind a load balancer…) — treat them as release failures.

Verified-crawler SEO previews

SEO image delivery is a separate, opt-in route. Configure seoCrawler to add GET/HEAD {basePath}/seo/:id; without it the route is disabled. The required verify(req,id) callback must authenticate the crawler using a trusted signal, such as provider-verified source-IP data or reverse-and-forward DNS. A User-Agent string is spoofable and must never be the only check. Archx does not bundle a crawler IP database.

seoCrawler: {
  verify: async (req, id) => verifyCrawlerFromTrustedSourceIp(req),
  watermark: '© Studio',
  maxWidth: 320,             // hard cap: 640 px
  maxWidthFraction: 0.5,     // hard cap: 0.9 of the master width
  quality: 55,               // hard cap: 75
  format: 'webp',            // or 'jpeg'
}

Only an exact true response permits an ordinary image; false/unverified callers get a non-disclosing 404, while verifier errors/timeouts fail closed. The preview is lossy, never wider than both configured caps, and carries a visible tiled PREVIEW ONLY watermark. It contains no Archx token or container and uses private, no-store response headers. Normal tokenized /i/:id delivery is unchanged. Include the preview URL in crawler-visible page HTML only when the page serves semantically equivalent content; keep the regular protected viewer as the human experience. See the operations guide for CDN bypass and trusted-proxy requirements.

Health checks & metrics

Small operational helpers ship with the server surface, so the middleware behaves like a good citizen in a real deployment:

import {
  createArchxMiddleware,
  createArchxHealthHandler,   // GET-able readiness probe (counts only, no secrets)
  formatPrometheusMetrics,    // handler.stats() as Prometheus text
  createJsonEventLogger,      // structured security-event log (PII hashed by default)
  installArchxShutdown,       // drain + close on SIGTERM/SIGINT
} from '@archnemix/image-protection/server';

const handler = createArchxMiddleware(options);
app.get('/healthz', createArchxHealthHandler(handler));

The full deployment runbook — CDN rules, multi-instance Redis state, key rotation, load testing — is OPERATIONS.md on GitHub.

Ownership proof

If a protected picture leaks anyway, three independent layers can point back to you — and to the specific copy it came from:

  • Visible watermark — baked into delivered pixels, per-buyer text supported ({buyer} placeholders).
  • StegoMark — an invisible, blind-detectable fingerprint carrying an owner/order id. On the recorded 1.30.2 StegoMark run (unchanged in 1.30.3), the 99-case fixed image-transform bench (JPEG q80/q50, resize, center/corner crop+resize, 90° rotation, flop, transpose, and brightness+JPEG), the robust profile recovered 85 marks. The matrix does not include screenshot or camera captures; the benchmark page describes its corpus, timing budgets, and limits.
  • Perceptual fingerprint — a content hash that recognizes the picture itself even if every mark is stripped, for lookup against your own registry.
# Later, given a suspect file found in the wild:
npx archx-detect suspect.jpg --expect "order-12345"

Honest grading: these are attribution evidence, ranked from strongest (exact StegoMark decode) to weakest (approximate perceptual match, corroborate before accusing anyone). Modern published diffusion attacks reduce or remove many tested invisible watermark schemes. Archx StegoMark has not been measured against full regeneration, so treat it as vulnerable rather than claiming a measured survival rate. The threat-model page gives the evidence boundary for each layer.

Honest limits

No tool can show a picture and make it uncopyable. Anything rendered on a screen can be photographed off the screen. Archnemix removes direct-file paths (right-click save, view-source, hotlinking to an image URL), makes replay harder with one-time claims, and can throttle bulk requests. It degrades some extraction paths (scrambled tiles, decoys, configured width caps, partial temporal frames) and marks what slips through for possible attribution. It is layered friction plus evidence — not DRM, and it never claims to be.

The full attacker-by-attacker account, including the cases where you should choose a different tool entirely (video DRM, C2PA provenance signing, neural watermarking services), is on the threat-model page.

FAQ

Will this stop screenshots?

No — nothing delivered to a browser can. Archnemix detects some capture signals, can blackout on print/screenshot heuristics, and the 60 Hz temporal interlace means a single-frame capture holds only part of the picture at full sharpness. A camera pointed at the screen always works; plan for attribution (marks + registry), not prevention.

Is the scrambled delivery encryption?

Scrambling is obfuscation with a 128-bit-seeded permutation — research shows permutation-only schemes fall to chosen-plaintext attacks, so we never call it secrecy. The actual boundary is server authorization + one-time tokens; AES-GCM payload encryption is available as an additional wire/obfuscation layer whose key, by necessity, reaches the authorized browser.

How much does it cost in performance?

The lite profile exists precisely for this: canvas rendering, no forensic layer, a small bitmap cache, and lazy loading keep per-image cost close to a plain optimized <img> pipeline. The elite profile spends more (WebGL, proof keys, interlace) — pick per image, per page, or per user tier.

Which browsers?

Current Chrome, Edge, Firefox and Safari. WebGL paths degrade to Canvas 2D automatically. The project’s browser suite runs in Chromium; Safari/WebKit and macOS are honestly listed as less-tested in the compatibility notes.

License?

BSD Zero Clause (0BSD): use, copy, modify, ship commercially — no attribution required, no permission to ask.

Further reading

DocumentWhat’s inside
READMEThe complete manual: every option, the full API, CLI reference.
OPERATIONSDeployment runbook: CDN, Redis multi-instance, key rotation, load tests.
Technical atlas (this site)Why every engineering decision was made, with the rejected alternatives.
Threat model (this site)Attacker-by-attacker: what holds, what degrades, what fails.
RESEARCH + PAPERSThe evidence log and the 52 sources behind the design choices.
CHANGELOGWhat each release actually changed — including the mistakes.