Use the access matrix and two independent clocks to choose who can read, who can write, and how long each kind of access lasts. Keep those dimensions separate because content availability and agent authority solve different problems. Recipients open the public content URL; teammates and agents use /v1. Content TTL governs the object, while token expiry governs the credential.
Sharing, access, and expiry
Access depends on the surface and operation
The resulting behavior is precise:
| Caller | Read open public content URL | Read passworded public content URL | Read through /v1 | Mutate owner object | Mutate instance object |
|---|---|---|---|---|---|
| Anonymous recipient | Yes | With share password | No | No | No |
| Valid API token | Yes | With share password on public content URL | Yes, without share password | Creator only | Yes |
| Signed-in human through hub | Yes | With share password on public content URL | With an API token | Creator only | Yes |
Common mistakes
The most consequential mistake is treating either control as a read ACL. A share password does not protect bytes from other valid tokens on the instance, and write_policy: "owner" does not restrict reads. If other token holders must not read the content, publish it to a separately controlled instance or use a storage system with per-reader authorization.
What a share password does
You can set a password when creating a site, set or clear it with PATCH, and set one on loose files with the corresponding JSON field or X-Energon-Set-Password header. Energon trims the value, accepts at most 128 characters, stores only a hash, and returns the password only in the response that just set it. Later GET and list responses expose only password_protected.
Browsers receive a password form. A successful form submission creates a path-scoped, HttpOnly, SameSite=Lax cookie for one week. Agents reading the public content URL send X-Energon-Password. Failed guesses are counted by both object path and source IP; after 20 failures within 15 minutes, even a correct attempt is temporarily rate-limited.
This disposable site flow exercises both read surfaces. Use a fresh slug and a non-demo password for real material:
export ENERGON_ORIGIN="https://energon.company.example"
export SHARE_PASSWORD="demo-only-passphrase"
test -n "$ENERGON_TOKEN" || { echo "Set the human-minted ENERGON_TOKEN" >&2; exit 1; }
SITE_SLUG="security-demo-$(date +%s)"
SITE_JSON=$(curl -fsS "$ENERGON_ORIGIN/v1/sites" \
-H "Authorization: Bearer $ENERGON_TOKEN" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg slug "$SITE_SLUG" --arg password "$SHARE_PASSWORD" \
'{slug:$slug, overwrite:false, password:$password, ttl:"7d", write_policy:"owner"}')")
PUBLIC_URL=$(printf '%s' "$SITE_JSON" | jq -r .url)
curl -fsS "$ENERGON_ORIGIN/v1/sites/$SITE_SLUG/files/index.html" \
-X PUT \
-H "Authorization: Bearer $ENERGON_TOKEN" \
-H "Content-Type: text/html" \
--data '<h1>Security review</h1>'
# Public-link read: the share password is required.
curl -fsS "$PUBLIC_URL" -H "X-Energon-Password: $SHARE_PASSWORD"
# API read: the API token is sufficient; no share password is used.
curl -fsS "$ENERGON_ORIGIN/v1/sites/$SITE_SLUG/files/index.html" \
-H "Authorization: Bearer $ENERGON_TOKEN"The POST returns the password because that request set it. Do not log that response in CI. Fetching the site later returns only whether protection is enabled.
What write policy does
The instance’s WRITE_POLICY is copied into every new object’s write_policy unless creation explicitly supplies owner or instance. Changing the instance default does not rewrite existing objects.
owner is keyed to the creating account’s stable identity, not just its current email spelling. It blocks another account’s PUT, PATCH, and DELETE. The creator alone may switch the object back to instance. In instance mode, any valid API token on the same host can update or delete the object, so it is best for collaborative scratch space rather than artifacts that need a single maintainer.
Create collaborative scratch work with instance, then have the creator switch it to owner when the artifact becomes authoritative. The stable URL remains unchanged while future mutations become creator-only.
Two independent clocks
content clock: create or PATCH ttl --------------------------> 410 Gone + purge
PUT changes bytes but does not move expiry
token clock: human mints token ----------------------------> 401 token_expired
object writes do not move token expiry mint a new tokenContent TTL belongs to each site or loose file:
- Creation uses the requested
ttlor the instance default. - PUT replaces bytes but does not extend the existing expiry.
- PATCH with
ttlresets expiry from the time of the patch. - At expiry, public and API reads return
410 Gone; request-time or scheduled cleanup removes R2 bytes and D1 metadata. - A missed cleanup does not make expired bytes readable because reads enforce the clock before serving.
Token expiry belongs to each credential:
- A human chooses from the token lifetime presets when minting; omission defaults to 90 days.
neveris available only when the instance allows unlimited tokens.- Expired and malformed non-null expiry values fail closed with
401 token_expired. - Expired tokens remain visible to their owner for diagnosis and revocation, but they cannot be renewed.
- Turning off unlimited tokens prevents future
nevermints; existing never-expiring tokens keep working until revoked.
Re-uploading a site before a deadline does not buy more time. If the content should remain available, PATCH its TTL explicitly and confirm the returned expires_at. If the agent token has expired, stop retrying and ask a human to mint a replacement at /tokens.
Reset the demo site’s content lifetime explicitly:
curl -fsS "$ENERGON_ORIGIN/v1/sites/$SITE_SLUG" \
-X PATCH \
-H "Authorization: Bearer $ENERGON_TOKEN" \
-H "Content-Type: application/json" \
--data '{"ttl":"7d"}'Continue reading
- Security model separates the identity, reader, and writer controls at a glance.
- Set safe instance boundaries turns those controls into deployment settings and smoke checks.
- HTTP API lists the site and file operations used in the examples.
- Configure an instance covers the complete runtime policy surface.