Status: Draft for implementation and public review
Protocol identifier: inkos.client/v1
Package format: inkos.package/v1
Service API: inkos.service/v1
This document is the normative, language-neutral contract for implementing an InkOS client. A conforming client can be a browser, an ESP32 device, a phone, a desktop application or another e-paper product. It does not need the InkOS renderer source code.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be interpreted as described by RFC 2119 and RFC 8174 when they appear in uppercase.
InkOS separates three kinds of data:
- Semantic content says what exists: a list, detail, reader or image page; text; ordinary images; content navigation; links to document UUIDs or HTTPS sources; and the exact reserved client actions defined below. It MUST NOT contain pixel coordinates, font names, screen sizes or device-specific branches.
- Screen/profile input says what the target can display: resolution, active physical panel size, rotation, palette, safe area, refresh alignment and rendering strategy.
- Rendered artifacts contain a PNG and a frame sidecar. Only this output layer may contain pixel hitboxes and pagination.
A client is deliberately simple. It selects an exact rendered variant, displays one page image, executes the navigation state machine, and verifies content before activation. It MUST NOT reinterpret semantic layout.
QR codes, barcodes and tickets are ordinary images. Dates, times, status and metrics are ordinary text. No client may depend on special QR, calendar, clock or metric primitives.
The trusted profile's physicalSizeMm MUST describe the active pixel area in
the panel's native orientation, not the product enclosure. A renderer derives
horizontal and vertical PPI from nativeSize and physicalSizeMm; supported
square-pixel panels MUST reject materially inconsistent axes.
Layout values are physical design policy, not content. The reference InkOS
renderer converts 160-PPI design units to native pixels before line wrapping and
pagination, and uses that conversion for typography, strokes and dividers,
spacing and padding, corner radii, icons and image chrome. A touch-enabled
profile also expands and clips renderer-owned interaction bounds to at least
7 mm in each axis. A request's relative fontLevel is applied after density
conversion and does not shrink these non-type primitives.
Orientation changes the logical size, rotation, safe-area edges and refresh
axes. It MUST NOT rotate or rewrite nativeSize or physicalSizeMm, so the
physical scale remains stable. None of this metadata may be copied into
inkos.content/v2; semantic JSON remains coordinate- and device-free.
Implementations declare one or more classes:
| Class | Required behavior |
|---|---|
render-client |
Display one server-rendered frame and execute its sidecar. |
package-client |
Verify, install and execute a local .ink package. |
online-client |
Resolve manifests/documents/frames through inkos.service/v1. |
generator-service |
Safely ingest a web source and build a verified .ink. |
Every client MUST publish:
- a semantic client version such as
1.0.0; - supported package format major versions;
- supported capability identifiers;
- supported screen profile IDs and versions;
- maximum archive and expanded sizes;
- supported image codecs and pixel formats.
Unknown REQUIRED capabilities make a package incompatible. Unknown optional metadata MAY be ignored only where its schema permits additional fields.
Every packaged document has a canonical RFC 9562 UUID encoded as a lowercase hyphenated string. Source-derived generators SHOULD use UUID v5 with a stable namespace and normalized canonical source URL so rebuilding the same source preserves navigation identity.
The packaged document envelope is:
{
"schemaVersion": "inkos.document/v1",
"uuid": "8b3d9199-814d-5f3f-86b8-c960fc14a2df",
"parentUuid": "f9bf6ed8-47e1-5f3f-95de-5562a8f28bd2",
"source": {
"url": "https://example.org/article",
"title": "Article",
"retrievedAt": "2026-07-16T14:00:00+08:00"
},
"content": {
"schemaVersion": "inkos.content/v2",
"id": "8b3d9199-814d-5f3f-86b8-c960fc14a2df",
"revision": 1,
"locale": "en",
"page": { "kind": "reader", "content": [] }
}
}content.id MUST equal uuid inside a package. The existing field name
documentId in an inkos.content/v2 link carries the target UUID string.
Optional page navigation is an ordered list of ordinary semantic links (for
example a site's main menu). It is independent of feed/crawl quotas and has no
coordinates; the renderer decides its device-specific placement and hitboxes.
- A package has exactly one
entryUuid. - The entry document MUST NOT have
parentUuid. - Every other bundled document MUST have exactly one
parentUuid. - The parent MUST exist in the same package for an offline-complete package.
- Parent relations MUST be acyclic and every chain MUST terminate at
entryUuid. - A link MAY target any bundled UUID. A link does not change canonical parentage.
parentUuid is the information architecture, not browser history. A client MAY
keep a short visit stack to restore a parent's prior page, but it MUST fall back
to the canonical parent at page zero when no matching visit is known.
The minimum durable state is:
activePackageId
activeDocumentUuid
pageIndex
selectedProfileId
orientation
fontLevel # -2, -1, 0, +1, +2
offline # false or true
Visit history is transient. Inside one package it contains
(documentUuid, pageIndex) pairs; an online client that follows a lazy source
link retains (packageId, documentUuid, pageIndex, sourceUrl?) so parent
navigation can atomically restore the previous verified package and page.
Clients SHOULD persist the active document/page and settings after a successful
frame display. They MUST NOT persist a destination before the frame has passed
validation and is displayable.
Each PNG page has one JSON sidecar identified by
inkos.frame-sidecar/v1. It contains:
- package, document and optional parent UUID;
- exact display variant ID;
pageIndexandpageCount;- PNG path and SHA-256;
- logical width and height;
- zero or more renderer-generated interactions;
- optional renderer-owned local
dynamicRegions.
An interaction is:
{
"id": "page.items[2].link",
"contentPath": "page.items[2].link",
"label": "阅读详情",
"bounds": { "x": 28, "y": 402, "width": 484, "height": 116 },
"targetUuid": "8b3d9199-814d-5f3f-86b8-c960fc14a2df"
}When a semantic link names an HTTPS page that is not bundled, the interaction
also carries targetUrl. Its targetUuid is the current packaged document as a
safe v1 fallback. An online client gives targetUrl only to
POST /sources/resolve; it MUST NOT fetch the target site itself. It stages and
verifies the returned exact package before switching. An offline-only client
keeps the current frame.
A packaged RSS/Atom item may instead carry fallbackUrl alongside its real
packaged-detail targetUuid. fallbackUrl and targetUrl are mutually
exclusive. The recovery URL MUST be credential-free HTTPS on the default HTTPS
port. A supporting client MUST try targetUuid first and MUST NOT contact the
source while that packaged document can be verified and activated. On a
revision/precondition change it MUST fetch the replacement manifest and retry
the complete manifest-bound transaction before using fallbackUrl. A client
MAY also use fallbackUrl after a small bounded number of fresh-manifest
retries still ends in a missing UUID, lineage mismatch, or artifact-integrity
failure. The currently active frame remains committed until the replacement
source package is fully verified and painted. Offline clients and clients
without this optional recovery keep the current verified frame.
New packages MAY use exactly these client-only collection actions:
inkos://collection/rssinkos://collection/website
They open the corresponding list stored by the device; they are not network
URLs. A client MUST compare the complete string against this allowlist, dispatch
it locally, and MUST NOT pass it to /sources/resolve, a browser navigation API
or a generic custom-scheme handler. A package or sidecar containing any other
inkos: URI, including a suffix, query, fragment or case variant, is invalid.
The fallback targetUuid remains the current packaged document so a client
without collection support safely keeps the current frame.
For backward compatibility, a client MAY accept the exact legacy action
inkos://collection/other, but it MUST dispatch it to the same stored
websites list as inkos://collection/website. Producers MUST NOT emit the
legacy alias in a new package. No generic inkos://collection/* namespace is
created by this exception.
Two additional exact actions are server-owned applications:
inkos://app/random-imageinkos://app/baidu-map
The client MUST dispatch these only to POST /api/ink/v1/apps/execute. It MUST
NOT pass them to /sources/resolve, navigate a browser to them, or fetch Picsum,
an image host, Baidu, or any other upstream itself. Suffixes, query strings,
fragments and case variants remain invalid. Each user entry generates a fresh
lowercase nonce and millisecond timestamp; orientation, font and page changes
reuse that identity so a random image does not change accidentally.
One exact action is device-local:
inkos://device/settings
The client MUST open its native/browser settings surface directly. It MUST NOT
send this action to /sources/resolve, /apps/execute, or a browser navigation
API. New PaperS3 home packages expose it as the top-right settings gear.
Bounds use half-open logical coordinates:
x <= point.x < x + width and y <= point.y < y + height. They MUST fit
inside the sidecar logical size. Hardware touch coordinates and browser CSS
coordinates MUST be normalized into this logical coordinate system before hit
testing.
When hitboxes overlap, the client MUST choose the smallest area. A tie is broken by sidecar array order. This lets an image link override a containing card while remaining deterministic.
A clock dynamicRegion contains a unique ID, HH:mm:ss/Asia/Shanghai,
refreshMs, fullRefreshEvery, exact logical bounds and a restricted
monospace black/white text style. It MUST fit inside the logical screen and MUST
NOT overlap an interaction. The paired PNG is always complete and contains a
static fallback. Unsupported clients ignore the region; they do not reject an
otherwise compatible package or attempt to infer a new layout.
An .ink image page may pair its normal gray4 PNG with a sourceImage object
in both the manifest page and frame sidecar:
{
"path": "sources/8b3d9199-814d-5f3f-86b8-c960fc14a2df/0000.jpg",
"bytes": 98139,
"sha256": "c51e2a15d5765f152f07054bf1cc82629730702cc9f644bd3a26fca507e3235b",
"mediaType": "image/jpeg",
"pixelSize": { "width": 540, "height": 960 },
"fit": "contain"
}This requires frame.source-image-jpeg-v1. A conforming implementation accepts
only 8-bit baseline sequential SOF0 JPEG with one or three components, verifies
the independently declared bytes/hash/SOF dimensions, and verifies that the
sidecar object exactly equals the manifest object. The ordinary imagePath and
imageSha256 continue to identify a complete gray4 PNG fallback.
A client declaring this capability MUST prefer the JPEG, decode it without a
source-specific tone curve, sharpening, dithering or grayscale quantization,
then preserve its aspect ratio and center it on opaque white using contain.
The final RGB-to-panel conversion remains the display driver's responsibility.
It MUST treat JPEG failure as package failure, not silently display the fallback
and report source-image success. A client without the capability rejects the
package at compatibility checking because the capability is required.
The source bytes may be losslessly rewritten from progressive to baseline JPEG entropy coding, but their decoded pixels MUST be unchanged. The generator MUST NOT resize or otherwise visually preprocess the source JPEG. This makes the source-image path suitable for comparing device-side image conversion against the independently rendered PNG path.
A touch client SHOULD use these interoperable defaults:
- swipe travel threshold:
max(48 logical px, 8% of the shorter edge); - dominant axis ratio: at least
1.25; - tap movement tolerance: at most
16 logical px; - right swipe: reserved and currently a no-op.
Visible controls and keyboard alternatives are REQUIRED in the normal browser simulator. A user-requested viewport-only/kiosk mode MAY hide all chrome and use gestures plus renderer hitboxes; its hitboxes remain keyboard-focusable. Recommended keys are Left/Backspace for parent, PageDown/ArrowUp for next page, and PageUp/ArrowDown for previous page/parent-at-start.
| Input | Precondition | Result |
|---|---|---|
| left swipe | parent exists | open canonical parent; restore its visited page when known, else page 0 |
| left swipe | root | no state change; report ROOT |
| up swipe | pageIndex + 1 < pageCount |
increment page index |
| up swipe | final page and a previous visit or parent exists | return to the previous visit; otherwise open the canonical parent |
| up swipe | final page at root with no previous visit | no state change; report END |
| down swipe | pageIndex > 0 |
decrement page index |
| down swipe | page 0 and parent exists | open parent using the same restoration rule |
| down swipe | page 0 at root | no state change; report ROOT |
| tap | point hits interaction | push current visit, open target UUID at page 0 |
| tap | no hit | no state change; report NO_HIT |
“Previous level” is the most recent successfully activated visit in the
client navigation stack. If that stack is empty, the active sidecar's
parentUuid is the fallback. A failed return MUST retain both the current
frame and the unconsumed history entry.
A client MUST reject a sidecar whose document UUID or page index does not match the active state. This prevents late network responses from navigating the wrong page.
For every state change:
- Resolve an exact display variant from one verified manifest.
- Online: bind the document, sidecar and image GETs to that manifest's strong
ETag using
If-Matchand bypass intermediary caches. - Load the document, sidecar and image as one transaction.
- Verify schema, declared byte lengths and SHA-256.
- Decode the image.
- Display/refresh successfully.
- Atomically commit the new navigation state.
If any step fails, the previous frame and state remain active. A browser SHOULD show a retry affordance; an e-paper device SHOULD preserve the retained image.
fontLevel is a render parameter, not a client-side decoration. Changing it
selects a different exact pre-rendered variant offline or calls the online
renderer. The client MUST NOT stretch text or otherwise pretend an unavailable
variant exists on hardware. displayMeta.invert remains in v1 payloads only as
a compatibility placeholder and MUST be false; clients MUST reject inverse
packages/frames and MUST NOT expose an inverse setting.
displayMeta.outputTuning is an optional server-raster parameter object for
gamma, contrast, blackPoint, whitePoint, sharpen, photoContrast,
quantization and supersampling. It is currently accepted only for gray4
PaperS3 rendering; omission selects the profile default. Clients MUST send only
validated fields and bounds from the service API
and MUST NOT reinterpret these controls as device-side image filters.
Offline behavior:
- If the exact variant exists, activate it at the same UUID and clamp the page
index to the new
pageCount - 1only when necessary. - If it is absent, keep the current setting/frame and report
VARIANT_UNAVAILABLE; do not silently substitute another font value.
Online behavior requests the missing variant with
POST /packages/{packageId}/render, verifies the returned PNG/frame/sidecar and
then activates it. The request MUST bind to the exact verified manifest using
manifestSha256 or If-Match; 412 means the client must reload the manifest
before retrying. The server recalculates pagination and may clamp the requested
page, so the returned frame/sidecar page index is authoritative. Orientation is
also part of a variant even if a device exposes only one orientation in its UI.
An on-demand PaperS3 response may contain X-Ink-Refresh-Hint: binary-text and
the same optional refreshHint in its decoded inkos.frame/v2 manifest. This
does not mean that every detail, list or reader page is text-only: the
server derives it from semantic image absence plus the final stable-gray4 PNG
histogram. A missing hint is accepted but MUST map to quality refresh; an
unknown/non-string manifest value or a header/manifest mismatch MUST reject the
dynamic frame transaction. A supporting hardware client may use a binary fast
waveform only after all ordinary payload checks, a local decoded-pixel check and
local transition checks pass. Image/map frames, orientation changes, first
presentation and periodic cleanup remain quality-refresh cases. Legacy packed
.ink frames that predate the field may retain a separately identified local
heuristic for compatibility. Browser clients may safely ignore the hint.
PaperS3 clients default to manual portrait. auto orientation begins following
IMU/browser sensor changes only after the user selects it; an initial sensor
sample MUST NOT silently turn the default frame sideways. The hidden settings
entry is a continuous five-second press inside the top 20% of the logical
screen. Releasing, leaving or moving outside tap tolerance cancels it. Settings
are applied as one verified render transaction and persisted only after success.
A client MAY recover from 412 PACKAGE_REVISION_CHANGED automatically once: it
re-fetches the manifest for the same packageId with cache bypass, repeats all
manifest/compatibility checks, and stages the same UUID/page/display request
against the new manifest. It MUST NOT consult the catalog or substitute another
package. The old package/frame remains active until the refreshed target has
passed document, frame, sidecar, hash and image-decode validation. A missing UUID,
invalid candidate or second 412 ends the attempt without changing active state.
offline=true is a strict content network policy: after activation, navigation
and settings MUST resolve only verified local artifacts. Telemetry MAY be queued
but MUST NOT block reading. offline=false uses the online service and MAY fall
back to an already verified local artifact while clearly reporting the source.
After a clock-bearing frame is committed, a browser SHOULD sample GET /time
and estimate the server offset from the request/response midpoint. If that fails,
it MAY use local browser time without failing the page. A PaperS3 device SHOULD
use SNTP directly after Wi-Fi association. Both update only the verified region,
align updates to refreshMs, stop on page change/settings/touch priority, and
use fullRefreshEvery only when the device waveform can clean the region without
leaving it visibly in an intermediate state. A PaperS3 client MAY defer that
cleanup until the next full-page transition because its quality waveform is too
slow for a one-second clock. They MUST NOT reflow surrounding content or treat
the archive as executable code.
Online clients use the versioned endpoints in service-api.md.
They SHOULD use If-None-Match only to revalidate the revision-floating manifest.
Every document, packaged frame and sidecar selected from that manifest MUST carry
its strong ETag in If-Match; missing bindings fail with 428, and a changed
lineage fails with 412 PACKAGE_REVISION_CHANGED before resource lookup. Clients
verify response hashes and cache only a fully verified document/page transaction.
A successful HTTP status alone is not proof that a frame is usable.
On 412, a client MAY refetch the same package manifest with cache bypass and
retry the same UUID/page/display transaction once from the document read onward.
It MUST discard any document/frame/sidecar bytes produced by the failed attempt,
MUST retain the previous online package until the replacement frame decodes, and
MUST surface a second 412. A length or SHA mismatch remains an integrity error;
it is never accepted as a revision-recovery shortcut.
The online renderer returns the same sidecar model used by an .ink package.
This is a core interoperability requirement: navigation behavior MUST not change
when switching between server and local package sources.
An online client MAY accept a web URL as a content source, but it MUST send only
that URL and its current displayMeta in
POST /api/ink/v1/sources/resolve. The client MUST NOT fetch the
target page, follow its links, parse its HTML, proxy its images or derive
semantic content locally. Those operations belong to the server security and
rendering boundary:
HTTPS source URL
-> server-side Chromium executes JavaScript
-> bounded post-render semantic DOM
-> Markdown
-> inkos.content/v2 semantic JSON
-> first requested PaperS3 16-level gray rendering
-> verified versioned .ink package cache
-> later display tuples rendered on demand
-> verified client activation
Markdown is a server-side intermediate form. It is not a package format and a
client MUST NOT render it or infer visual layout from source HTML/CSS. Only
inkos.content/v2, the trusted screen profile and the renderer determine the
frame. No source script, DOM or Markdown is delivered to or executed by the
client.
The server filters hidden and small decorative source images before semantic
JSON generation, using Chromium's rendered dimensions plus accessibility and UI
semantics. Such source chrome is therefore absent from the package and requires
no client heuristic. Clients MUST treat every image that remains in verified
inkos.content/v2 as content.
The client handles source resolution as follows:
- Submit
{ "url": "https://example.org/", "displayMeta": { ... } }to/sources/resolve. The server's realtime request uses Chromium,maxDepth=0andmaxDocuments=1; only the current URL and first display tuple are in the foreground critical path. This endpoint never requests the full display Cartesian product. - For
202 Accepted, retain the current frame and use the returnedjob,jobId,statusUrland optionaleventsUrlto observe progress. PollingstatusUrlis sufficient; SSE is optional. - On
failedorcancelled, keep the current package/frame active and report the server error. - On completion or an immediate cached
200 OK, take the authoritativepackageIdfrom the completed service response/job. Do not guess fromexpectedPackageIdand do not select an unrelated first catalog entry. - Resolve that exact package manifest, document, sidecar and frame; retain the manifest SHA-256 for on-demand rendering; verify compatibility, declared byte lengths and SHA-256 values; decode the frame; then atomically activate it under the loading-and-commit rules in section 6.3.
The current service publishes a valid realtime draft at revision 17, capped at 16 feed items or four detail blocks and six image-preview documents. It then builds a revision 18 archive with a deeper bounded crawl and full bounded image-preview expansion at low priority. A draft therefore guarantees image-fullscreen taps only for its first six images; other images remain inline without a fullscreen target, and the archive supplies all admitted previews. On a later resolve or refresh, a client may receive the higher verified revision for the same package ID. These numbers are service revision policy, not protocol constants: clients MUST accept the authoritative package revision and MUST NOT delay first-frame activation waiting for the archive upgrade.
A fresh verified source package returns synchronously. When the service marks a
response stale: true, it has returned the old verified package immediately and
started asynchronous revalidation; the client MAY activate that package under
the same verification rules. X-InkOS-Source-Cache (hit, stale, or miss)
is diagnostic only and never replaces package validation.
The PaperS3 browser client supports the source as a query parameter, including in its viewport-only mode:
/papers3-client?fullscreen=1&url=https%3A%2F%2Fjandan.net%2F
The query value MUST be percent-encoded. This is especially important for source
URLs containing & or #, which would otherwise be interpreted as part of the
outer client URL rather than the source URL. Receiving a query value never
changes the trust boundary: the browser still sends it to InkOS and never
requests the source site directly.
The browser keeps a copyable exact location in the address bar:
/papers3-client?fullscreen=1&url=<encoded-source>&package=<package-uuid>&uuid=<document-uuid>&page=0
package, uuid and page identify the exact package lineage, document and
page. A recipient stages and verifies that exact location; a missing/mismatched
UUID is an error and MUST NOT fall back to the first catalog item. The url
parameter may be omitted when package is present.
POST /api/ink/v1/apps/execute accepts only the two exact actions above. The
random-image action may also carry the device's ordered persistent images
collection (at most 16 entries) and a zero-based pageIndex. Each entry is an
HTTPS image URL, and each URL is one full-screen page; up/down gestures move
through the collection. The editable default is
https://picsum.photos/540/960?random=1. The server replaces only that exact
default's random value with the request nonce, so page/settings retries keep
the same subject. inkos://app/random-image remains only the client action
that opens the viewer; new clients MUST NOT store it as an image collection
URL. For upgrade compatibility the service still accepts the retired action
alias and exact former grayscale default
https://picsum.photos/540/960?grayscale&random=1; those legacy values are
also converted to a nonce-sized non-grayscale request. Other user-authored
URLs, including custom grayscale URLs, remain unchanged.
The map action has one page. The server obtains a BD-09 location from the requesting server's public IP and requests a matching Baidu static map. Its credential is server configuration and MUST NOT appear in a package, request, device setting, browser state, log, response body or upstream URL exposed to a client.
The response declares its concrete processing in X-Ink-App-Image-Mode.
Image Viewer photos use
photo-papers3-slideshow-gray16-rgb-png-v3: after cover geometry, the server
matches the proven PaperS3 slideshow preparation: grayscale, 0.5% cutoff at
both autocontrast ends, contrast 1.08, unsharp mask radius 1 / 65% / threshold
3, then serpentine Floyd-Steinberg diffusion against the sixteen mathematical
levels 0, 17, …, 255. It encodes the selected indices as stable RGB centres
8, 24, …, 248. Each value therefore maps to one native PaperS3 level at every
driver Bayer position rather than being spatially mixed with an adjacent level.
Maps use
diagnostic-raw-colour-png-v1: after contain geometry, their decoded RGB
pixels remain unchanged. Both no-store PNGs have an 8-bit RGB or RGBA IHDR
(colour type 2 or 6) and no PLTE; the photo's RGB bytes contain only stable
native-gray centres, while map RGB remains unmodified. PaperS3 still converts
decoded RGB to its physical 16 gray levels, and ordinary .ink package frames
remain strict indexed gray4. Before activation the client verifies the echoed
action/nonce/timestamp/page, declared length, SHA-256/ETag, frame manifest,
sidecar, exact PaperS3 variant, pagination, PNG geometry and the
action-appropriate image mode. A failed request or decode leaves the previous
frame and app identity active. The native PaperS3 client maps only the exact
photo mode to its strongly typed PaperS3PhotoGray16 display profile, which
uses a high-contrast white clear, full 16-gray quality body, and black/white
endpoint reinforcement. The exact map mode remains on generic quality refresh.
Clients MUST NOT infer the photo profile from contentType: "image" because
maps and ordinary package images share that semantic type.
A client with no explicit URL/package/document request loads the catalog's
declared defaultPackageId and defaultEntryUuid; it MUST NOT infer the home
package from list order. The InkOS PaperS3 browser client downloads that package's
.ink artifact, runs the normal archive integrity checks, and installs it in the
offline slot as its no-parameter fallback. The same built-in archive is offered
as an explicit Demo beside the local-file picker. An explicit URL or package deep
link takes precedence and MUST NOT silently fall back to the home if validation
fails.
The InkOS PaperS3 default home is itself a normal inkos.package/v1 archive: the
application grid, browser site list, month calendar and date almanac pages are
ordinary packaged documents connected by UUID links and canonical parents. A
client MUST NOT add a private “home screen” renderer or special app-navigation
opcode.
Following a home-package targetUrl, including one reached from the offline
fallback, uses the server-owned URL flow in section 8.1 and pushes the current
source mode plus document/page visit. Returning restores the verified offline
home frame without downloading the target website in the client. A package-only
share/deep link MUST omit any URL left in an input field or previous visit;
otherwise the receiver could resolve one source while asserting another package
identity.
The current catalog home declares the portrait and landscape base tuples at
font level 0. It intentionally does not enumerate all font combinations. Online
clients use the manifest-bound package renderer for any absent tuple. Offline
clients keep the strict VARIANT_UNAVAILABLE
behavior from section 7.
In fullscreen=1, PaperS3 displays only the rendered frame and transparent
hitboxes. The first user gesture may request the browser Fullscreen API with
navigation UI hidden. An installed PWA can launch without browser chrome;
installation/service workers require a secure context (HTTPS, except browser
localhost exemptions).
In this protocol “content OTA” means installing a .ink data package. It is not
firmware OTA and MUST NOT execute package-provided code.
Clients implement two logical slots:
active slot -> currently trusted package
staging slot -> incomplete or newly downloaded package
Installation sequence:
- Download/copy to staging using a temporary filename.
- Enforce compressed and expanded size limits.
- Validate normalized archive paths and reject duplicate entries.
- Parse manifest and check format/client/capability/profile compatibility.
- Verify every declared length and SHA-256, including optional source images.
- Validate UUID uniqueness, parent graph, interaction targets and exact manifest/sidecar source-image equality.
- Validate all PNG dimensions and any baseline JPEG SOF dimensions/profile.
- Decode the preferred entry artifact for the current settings.
- Flush staging data, then atomically switch the active pointer.
- Keep the previous slot until the new entry frame displays successfully.
Power loss or validation failure leaves the prior active package intact. Clients SHOULD expose package ID, revision, source and verification result in diagnostics.
Shared symbolic errors:
| Code | Meaning |
|---|---|
ROOT |
Parent navigation requested at package root. |
END |
Next page requested at the final page. |
NO_HIT |
Tap did not hit an interaction. |
STALE_FRAME |
Sidecar does not match active navigation state. |
UUID_NOT_FOUND |
Requested target is unavailable. |
VARIANT_UNAVAILABLE |
Exact display setting/profile variant is absent. |
PACKAGE_INCOMPATIBLE |
Version, capability or profile check failed. |
PACKAGE_CORRUPT |
Schema, size, hash, path or graph validation failed. |
SOURCE_OFFLINE |
Network use was requested while strict offline mode is active. |
RENDER_FAILED |
Online rendering did not produce a verified frame. |
No-op navigation (ROOT, END, NO_HIT) is not a fatal error. Devices MAY use
a small visual/haptic cue, but e-paper clients SHOULD avoid a full refresh just
to report a no-op.
.inkv1 is data-only. Clients MUST ignore and SHOULD reject executable files, scripts, HTML event handlers and undeclared archive entries.- Archive paths MUST be normalized relative UTF-8 paths with no
..,., empty segments, backslashes, NULs or absolute prefixes. - Clients MUST enforce compressed file count, compressed size, expanded size and individual image decode limits before activation.
- Generators MUST defend against SSRF, validate every redirect and Chromium subresource, reject private or link-local destinations, cap response/DOM/output bytes and set hard timeouts.
- This deployment accepts
198.18.0.0/15as a controlled local proxy/TUN Fake-IP egress range; it does not relax loopback, RFC1918, link-local, CGNAT or IPv6 ULA rejection. - A generator MAY execute source JavaScript only inside its server-side capture
boundary. The current service shares a Chromium process for latency but uses a
fresh non-persistent context for each request, blocks service workers,
downloads, WebSockets and popups, waits for
DOMContentLoadedplus a short stability window (nevernetworkidle), performs at most two bounded scrolls, and enforces a 10-second capture deadline. Only a bounded sanitized rendered DOM is converted to Markdown and then semantic JSON. Clients MUST NOT execute any of those source scripts. - Packages SHOULD record source URL, retrieval time and content license. Client authors remain responsible for license/attribution presentation.
The repository's server/lib/ink/*.test.ts fixtures are the initial executable
test vectors. Before a public v1 release, fixtures MUST be exported as plain JSON
and binary .ink files so any language can run the same cases without TypeScript.
At minimum a client test suite MUST cover:
- every row in the navigation table;
- stale sidecar rejection;
- overlapping and boundary hitboxes;
- parent history restore and canonical fallback;
- exact variant selection and unavailable-variant behavior;
- corrupted size/hash, unsafe path, duplicate path and undeclared file rejection;
- missing UUID, parent cycle and dangling interaction rejection;
- incompatible format, old client, missing capability and unsupported profile;
- source JPEG without its capability, non-image source attachment, progressive or unsupported JPEG, source size/hash/dimension mismatch, and unequal manifest/sidecar source metadata;
- interrupted staging install preserving the previous active package.
Backward-compatible additions use new optional fields or capability identifiers.
A breaking archive or state-machine change increments the schema major version.
Clients MUST NOT infer behavior from a field they do not understand. Experimental
capabilities use an x-<vendor>. prefix and MUST NOT appear in the core required
set of a generally distributed package.