CRYSTALBOUND DEVELOPMENT UPDATE HOST

Worker: crystalbound-updates-dev
Domain: updates-dev.aletheiagames.com (the live updates domain is reserved)
Private R2 bucket: crystalbound-updates-dev
Feed: https://updates-dev.aletheiagames.com/dev/

This is development infrastructure, not a promotion to dev/live Git branches.
The bucket remains private with r2.dev and R2 custom-domain public access OFF.
The Worker's custom domain is separate from R2 public-bucket custom domains.
Download credentials allow only GET/HEAD for /dev/manifest.json, /dev/news.json and
/dev/objects/<SHA256>. No directory listing or arbitrary file route exists.
A separate server-only operator credential permits GET/PUT of /admin/news,
which can access only dev/news.json. It cannot publish game releases.
/health is public and contains no release, storage, or credential details.

DOWNLOAD ACCESS
New launcher packages use game-account sign-in. Set the Worker secret
DOWNLOAD_TICKET_KEY_HEX to the same 32-byte random key, encoded as 64 lowercase hex
characters, read by the game login service. Keep the key outside Git, client
packages and logs, in a file readable only by the login service identity. The
server repository's launcher-auth directory contains the matching issuer patch.

After password and configured two-factor checks, a request with
launcher_downloads:true receives update_token alongside the existing game session.
This is an HS256 JWT limited to audience updates-dev.aletheiagames.com, channel dev,
scope download, issuer crystalbound-login, account ID, issue/expiry times and a
random nonce. The Worker accepts at most 900 seconds of validity. The game password
and game session hash never reach this service. Tickets exist only in launcher
memory and travel in HTTPS Authorization headers, never URLs or saved preferences.
Set accountUpdates:true and requireUpdateAccessCode:false in the launcher config.

Authorization is stateless: an already-issued ticket remains valid until expiry
even after an account ban, password change or deletion. Rotating the shared key
invalidates all outstanding tickets immediately. Update the Worker secret and
server key together during maintenance; a mismatch fails closed. This key grants
only development download access, not game login or Cloudflare administration.

LEGACY INVITATION COMPATIBILITY
Set the Worker secret DOWNLOAD_TOKEN_SHA256 to the lowercase SHA256 of a random
32-byte base64url access code (43 characters, no padding). Do not set the raw code
as a Worker variable. The launcher sends the code over HTTPS in a Bearer header;
never put codes in URLs, Git, release packages, or logs. Rotate the code by replacing
the secret. ACCESS_EXPIRES_UTC in wrangler.jsonc expires this test on November 5,
2026. Missing/invalid/expired invitations cannot authorize downloads. This invitation is
read-only access to test downloads, not a Cloudflare API token or game password.

Set requireUpdateAccessCode:true and the above feed in the development launcher's
crystalbound.json. Enter the supplied invitation in Options; it lasts only until
the app closes. This compatibility route supports the previous owner-tested build
while the account-based replacement is validated. Account tickets do not depend
on the old invitation's expiration date. Remove the legacy secret after migration.
Do not silently switch this service to public access to bypass authentication.

LOCAL CHECKS
  npm ci
  npm test
  npm run check

Set CPP_TICKET_FIXTURE to the native issuer test's cpp-ticket.json when running
npm test to include the C++-to-Worker interoperability test. That fixture uses a
public test key; never supply the production signing key to tests.

AUTHORIZATION AND DEPLOYMENT
Use Wrangler's browser/device OAuth on the server. Select only account/user read,
Workers write (includes R2 access), scripts/routes write, zone read, and the
automatically requested offline access. Complete both Cloudflare sign-in AND
Wrangler's authorization screen; reaching the sign-in page is not authorization.
Prefer --use-keyring to keep OAuth credentials in the Windows credential store.
Never copy Cloudflare credentials into the game client or repository.
Before deployment, confirm the account and that this exact bucket belongs to it.
Deploying creates the Worker/custom domain and its HTTPS certificate. A missing
download secret fails closed; set the secret through Wrangler after deployment.
Use only the Workers Free plan for this first development test.

RELEASE PREPARATION
  node scripts/prepare-release.mjs CONFIG_JSON SIGNED_FEED NEW_STAGING_DIRECTORY
  node scripts/publish-release.mjs CONFIG_JSON STAGING_DIRECTORY ACCOUNT_ID --publish-dev

This offline command verifies RSA-PSS signatures, the dev channel, expiry, managed
paths, file sizes and hashes, then stages only the intended objects. It never copies
the retail baseline, credentials, the whole game folder, or repository contents.
Upload according to upload-plan.json: payload objects first, a historical manifest,
initial news if absent, and finally dev/manifest.json. Existing operator-managed
news is retained even when the staged release has different news. Verify uploaded
object sizes/hashes before
changing the current manifest. Reject lower/equal sequences with different bytes.
Keep history/<sequence>-<release>.json and objects immutable. Resuming identical
uploads is safe. Run launcher verification against the HTTPS feed after publishing.
Run one owner-controlled publisher at a time. The uploader detects changed release
pointers before committing, but does not implement a distributed publication lock.

DEPLOYMENT STATUS — OCTOBER 6, 2026
Deployed at https://updates-dev.aletheiagames.com. R2 public development URL is
disabled; the bucket has no public custom domains. Wrangler authentication is
encrypted with the key in Windows Credential Manager. The signed development
release qiqirn-prayer-launcher-alpha.1 (sequence 1) was uploaded and every object
read back and SHA256 verified. The Worker now supports both account tickets and
legacy invitations. The matching login service and owner-laptop launcher were
installed on October 6 after logout and closed-client checks, with verified backups.
No dev/live Git branch has been promoted.

Validation: 13 hosting checks (including native-issued ticket interoperability),
30 core/bridge checks, and four desktop-controller integration checks passed.
Browser tests cover account-first updates, permission retry, expiry, Play gating,
credential clearing, legacy compatibility and three window sizes. Builds and
Wrangler deployment validation passed. The previous invitation build installed
the signed release through HTTPS and launched the owner's game successfully.
Seven live checks now pass: legacy login, invalid-password rejection, issued ticket,
signed HTTPS installation, forged-ticket rejection, missing-OTP rejection, and
two-factor login plus protected download access. Disposable accounts were removed.
On October 6 the owner confirmed successful sign-in and game launch through the
new launcher without a development code. UAC prompt behavior was not separately
reported. Legacy invitation compatibility and rollback backups remain available.

News publishing is connected to the server control tool's Launcher news view.
The installed launcher reads this feed after account sign-in; no launcher rebuild
is needed to publish a post. The public launcher
download website is now published at https://aletheiagames.com; its source and
deployment notes are in ../../site. It uses a separate bucket and leaves this
account-protected update service unchanged. Live-channel
publication, and game-client installation remain separate work.

LAUNCHER SELF-UPDATES — 0.2.1
Public GET/HEAD routes /launcher/dev/manifest.json and /launcher/dev/objects/HASH
serve only signed launcher releases under a separate R2 prefix. These do not use
or expose game account credentials. The manifest is limited to 64 KiB and binary
objects to 300 MiB. All existing game routes retain their authorization checks.
Use scripts/publish-launcher.mjs CONFIG FEED --publish-dev after Tools signs a
launcher release. The matching complete ZIP and source must also be published on
the website. See ../SELF-UPDATES.txt for transaction/recovery details.

Set LAUNCHER_MIGRATION_VERSION to 0.2.0 only after the new website package exists.
Requests without X-Crystalbound-Version (the original launcher), or with a lower
version, then receive a pinned manual-download note in news. Self-updating clients
receive the original feed. This transformation never rewrites R2 news or editor
drafts. New clients report version in their User-Agent and dedicated header.

NEWS PUBLICATION
NEWS_PUBLISH_TOKEN_SHA256 holds the SHA256 of a separate random 32-byte base64url
operator token. Only its hash is stored by the Worker. The raw token stays in the
server's restricted var/launcher-news-secrets/publisher.token file, readable by
the owner, Administrators, and SYSTEM. It never enters a launcher package, browser,
URL, Git repository, or Cloudflare API client at runtime. Do not reuse download
tokens for publication. The boot-started panel does not require Wrangler OAuth.

GET /admin/news returns the current feed and quoted R2 ETag. PUT requires the
operator Bearer token, Content-Type: application/json, and exact If-Match ETag.
R2 checks that precondition atomically; stale writes return 412. The route accepts
only the strict launcher schema, at most 20 posts / 64 KiB, and no draft fields.
News is plain text delivered over authenticated HTTPS, not executable content.
It is separate from the signed game-file manifest and cannot modify that manifest.

The panel preserves local drafts, backs up previous hosted feed bytes, and checks
the hosted SHA256 after publication. Failed/uncertain delivery remains pending
with an explicit retry. It will not overwrite a feed changed by another publisher.
Review such conflicts manually before reconciling; do not reset the version blind.
Setup and panel deployment scripts live in the server repository's operations/.
Rotate this operator token by updating its protected local file and Worker hash
together; game-account download keys are independent and remain unchanged.
