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:
| Import | What it is |
|---|---|
@archnemix/image-protection | The browser viewer and loader (Archx.load.image(…)). |
…/server | The Node middleware that authorizes viewers and serves one-time derivatives. |
…/scramble | The environment-agnostic scramble/encrypt engine used by both sides. |
…/redis | Shared 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.
| Profile | Optimizes for | Typical use |
|---|---|---|
lite | Least CPU, memory and bytes. Canvas renderer, smaller cache, forensic marking off. | Catalogues, thumbnails, low-end phones, thousands of images per page. |
performance | The balance: strong protection without starving the device. Forensic marking on. | Most galleries, shops and portfolios. Start here. |
elite | Maximum 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.
| Option | Default | What it does |
|---|---|---|
profile | — | 'lite' | 'performance' | 'elite' — fills unset options from the profile above. |
lazy | true | Load only when scrolled near the viewport (tune with rootMargin). |
tokenEndpoint | null | Required for { id } sources; set it to your middleware route (for example, '/archx/token/') so the viewer can mint a one-time delivery token. |
watermark | null | Visible watermark text (or an object for position/opacity). |
forensic | true | Invisible per-viewer forensic marking layer (the lite profile turns it off). |
temporalInterlace | null | Splits 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. |
guards | most on; DevTools off | Right-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:
- Keep masters outside every public root. If the
original file is web-addressable, nothing else matters.
imagesDirmust not be inside a static mount. Declare each public root withpublicDir/publicDirsso Archx can check for overlap; startup cannot detect undeclared host mounts, which remain your responsibility. authorizeis required unless public access is explicit. Omitting bothauthorizeandallowAnonymous:truefails at startup. The latter is an intentional public-catalog exception, not a safe default.- Use a real secret. 32+ random characters from an environment variable or secret manager; the startup audit flags weak, shared, or well-known values.
- Cap the resolution. The default
maxWidthFractionis1, so an entitled viewer may receive native-width pixels when other caps permit. Set it below1(for example,0.6) to make “preview only” a server-enforced property, not a client suggestion. - 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
| Document | What’s inside |
|---|---|
| README | The complete manual: every option, the full API, CLI reference. |
| OPERATIONS | Deployment 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 + PAPERS | The evidence log and the 52 sources behind the design choices. |
| CHANGELOG | What each release actually changed — including the mistakes. |