Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ full engineering record.

## [Unreleased]

## [0.3.29] — 2026-07-31

- Made Realm gestures recover cleanly when a menu, record, or inactive
presentation interrupts an in-progress drag or pinch.
- Added bounded recovery for embedded Back navigation when a host traverses
history without delivering its expected event.
- Kept overflow gathering portraits visual-only while preserving one bounded,
keyboard-accessible control lane and current public keep labels.
- Tightened Farcaster token lifetime, sensitive endpoint, browser-isolation,
and access-request queue boundaries without changing admission decisions.
- Made exact Worker dispatch retries terminally idempotent after normal return,
including when the Worker has since begun a later valid assignment.
- Left castle ownership, gathering rates, balances, terrain, and persistent
world data unchanged.

## [0.3.28] — 2026-07-31

- Brought Genesis 001 into a portrait-first Farcaster Mini App with a compact
Expand Down Expand Up @@ -310,7 +325,8 @@ full engineering record.
Lowlands, a first keep, Farcaster sign-in, and an admission-gated shared-world
foundation.

[Unreleased]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.28...HEAD
[Unreleased]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.29...HEAD
[0.3.29]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.28...v0.3.29
[0.3.28]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.27...v0.3.28
[0.3.27]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.26...v0.3.27
[0.3.26]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.25...v0.3.26
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Genesis 001 is a persistent, invite-only 10,000-cell Lowlands realm with 100 permanent castle sites gathered near its founding district. Each admitted founder enters through a verified Farcaster identity, receives one durable keep, and returns to a world that remembers its coastline, twelve rivers, wintering north, sunscoured south, shaped forests, resource sites, Workers, wagons, and public keeper portraits. Four permanent Workers can gather Food / Wood / Stone / Gold, settle production automatically, and return home on command.

Alpha 0.3.28: The Realm in Hand is live in ordinary browsers and as a portrait-first Farcaster Mini App. Verified players who are still outside the gate can request access for manual review. The intended core strategy loop is not playable yet: Warpkeep is a one-person, open-source experiment—not a finished MMO or financial product. Participation earns no token reward, airdrop, financial return, guaranteed reward, or future value.
Alpha 0.3.29: The Realm Holds is live in ordinary browsers and as a portrait-first Farcaster Mini App. Verified players who are still outside the gate can request access for manual review. The intended core strategy loop is not playable yet: Warpkeep is a one-person, open-source experiment—not a finished MMO or financial product. Participation earns no token reward, airdrop, financial return, guaranteed reward, or future value.

![Development preview of Genesis 001 showing the Lowlands and an open Wheat Farm inspection panel.](docs/reference/screenshots/2026-07-22-realm-wheat-farm-preview/warpkeep-realm-wheat-farm-preview-f3b1f7e598c543d6.png)

Expand All @@ -29,7 +29,7 @@ Open the local URL Vite prints; shared Alpha access stays off by default. Contri

| State | Today |
| --- | --- |
| ✅ Live | Alpha 0.3.28: The Realm in Hand is live and invite-only in browsers and the Farcaster Mini App. |
| ✅ Live | Alpha 0.3.29: The Realm Holds is live and invite-only in browsers and the Farcaster Mini App. |
| ✅ World | Genesis 001 persists 10,000 cells, a coastline, twelve one-cell rivers, colder northern and drier southern frontiers, and 100 permanent castle sites near the founding district. Founders return to one durable keep, explore the Lowlands up to its fog, and inspect nearby founders through their public username / portrait / castle. |
| ✅ Authority | FID is the durable identity; handles and portraits are bounded presentation metadata. Farcaster sign-in uses a browser-bound, least-privilege bridge. The browser presents; the server decides admission, ownership, resources, timers, and saved state. A private access request asks for manual review and grants nothing by itself. |
| ✅ Resources | Each keep privately holds Food / Wood / Stone / Gold. Food, Wood, and Stone come from authoritative terrain yield and can also be gathered at Wheat Farms, Logging Camps, and Stone Quarries; Gold comes from Gold Mines. Completed yield settles without a Claim step, and occupied sites keep their gathering story in one record. The browser never invents balances. |
Expand All @@ -54,7 +54,7 @@ Open the local URL Vite prints; shared Alpha access stays off by default. Contri
- **Architecture:** The [technical architecture](docs/technical-architecture.md) explains what the browser shows and what the server decides.
- **Roadmap:** The [roadmap](docs/design/roadmap.md) and [game direction](docs/design/warpkeep-direction.md) separate today's game from later plans.
- **Authentication:** The [Farcaster integration](docs/farcaster-integration.md) guide covers sign-in, privacy, and public configuration.
- **Release:** The [Alpha 0.3.28 release notes](CHANGELOG.md#0328--2026-07-31) describe The Realm in Hand.
- **Release:** The [Alpha 0.3.29 release notes](CHANGELOG.md#0329--2026-07-31) describe The Realm Holds.
- **Licensing:** [LICENSING.md](LICENSING.md) explains release rules; [asset provenance](ASSETS-LICENSE.md) records where media came from and what permissions apply.
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md) covers checks and provenance; the [Realm Council issue forms](https://github.com/ael-dev3/Warpkeep/issues/new/choose) accept privacy-safe bugs and ideas.
- **Security:** Report sensitive issues privately through [SECURITY.md](SECURITY.md), never through a public issue.
Expand Down
2 changes: 1 addition & 1 deletion docs/farcaster-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ordinary browsers use Sign In with Farcaster (SIWF), while a verified Farcaster
Mini App host may use Quick Auth. Neither path is a wallet connection,
client-owned identity, admission grant, or Terms acceptance.

Alpha 0.3.28 keeps backend protocol 3 and authentication contract v2; admission
Alpha 0.3.29 keeps backend protocol 3 and authentication contract v2; admission
remains gated. Production configuration and founder identities belong in the
private operator record, not this guide.

Expand Down
2 changes: 1 addition & 1 deletion docs/releases/versioning.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Versioning and releases

Warpkeep uses semantic versions for the product and Git commit SHAs for builds.
Alpha `0.3.28` is the live **The Realm in Hand** release. A version is
Alpha `0.3.29` is the live **The Realm Holds** release. A version is
described as live only after its protected review, matching client deployment,
and production smoke test all succeed.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "warpkeep",
"private": true,
"version": "0.3.28",
"version": "0.3.29",
"description": "A Farcaster-connected persistent strategy world in active Alpha development.",
"license": "Apache-2.0",
"homepage": "https://warpkeep.com",
Expand Down
1 change: 0 additions & 1 deletion scripts/qa-observer/rendered-mobile-map-gesture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ export async function applyRenderedMobileMapGestureInteraction(
fixtureHost.append(fixtureControl);
const visibleWorldControls = [...document.querySelectorAll(
'.realm-castle-label, .realm-worker-presence-marker, '
+ '.realm-resource-occupant-presence, '
+ '.realm-resource-occupant-marker'
)].filter((control) => {
if (!visible(control)) return false;
Expand Down
5 changes: 3 additions & 2 deletions scripts/qa-observer/rendered-webgl-browser-probe.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -571,11 +571,12 @@ export type RenderedWebglResourceOccupantEvidence = Readonly<{
markerHitTestable: true;
overviewPresenceDirectHit: boolean;
overviewRecordCorrect: boolean;
overviewTargetPassiveOnly: boolean;
overviewTargetControlOnly: boolean;
passivePresenceVisualOnly: boolean;
presenceComputedVisible: boolean;
presenceAvatarGeometryValid: boolean;
presenceGeometryValid: boolean;
presenceDelegatedActivation: boolean;
overviewControlActivation: boolean;
presenceHitTestable: boolean;
presencePointerActivatable: boolean;
presencePortraitElementPresent: boolean;
Expand Down
93 changes: 55 additions & 38 deletions scripts/qa-observer/rendered-webgl-browser-probe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,11 +1355,12 @@ export function parseRenderedWebglResourceOccupantEvidence(value) {
'markerHitTestable',
'overviewPresenceDirectHit',
'overviewRecordCorrect',
'overviewTargetPassiveOnly',
'overviewTargetControlOnly',
'passivePresenceVisualOnly',
'presenceComputedVisible',
'presenceAvatarGeometryValid',
'presenceGeometryValid',
'presenceDelegatedActivation',
'overviewControlActivation',
'presenceHitTestable',
'presencePointerActivatable',
'presencePortraitElementPresent',
Expand All @@ -1384,11 +1385,12 @@ export function parseRenderedWebglResourceOccupantEvidence(value) {
'cameraNeutralWhileOpen',
'overviewPresenceDirectHit',
'overviewRecordCorrect',
'overviewTargetPassiveOnly',
'overviewTargetControlOnly',
'passivePresenceVisualOnly',
'presenceComputedVisible',
'presenceAvatarGeometryValid',
'presenceGeometryValid',
'presenceDelegatedActivation',
'overviewControlActivation',
'presenceHitTestable',
'presencePointerActivatable',
'presencePortraitElementPresent',
Expand Down Expand Up @@ -7208,11 +7210,12 @@ export async function applyRenderedWebglResourceOccupantInteraction(
markerHitTestable,
overviewPresenceDirectHit: false,
overviewRecordCorrect: false,
overviewTargetPassiveOnly: false,
overviewTargetControlOnly: false,
passivePresenceVisualOnly: false,
presenceComputedVisible: false,
presenceAvatarGeometryValid: false,
presenceGeometryValid: false,
presenceDelegatedActivation: false,
overviewControlActivation: false,
presenceHitTestable: false,
presencePointerActivatable: false,
presencePortraitElementPresent: false,
Expand Down Expand Up @@ -7247,7 +7250,7 @@ export async function applyRenderedWebglResourceOccupantInteraction(
&& map.getAttribute('aria-busy') === 'false'
&& !map.hasAttribute('data-camera-interacting');
const projectionSnapshot = () => {
// A selected peer may legitimately move from the passive pointer lane
// A selected peer may legitimately move from the passive presentation lane
// into the bounded keyboard-control lane while its record is open.
// Both lanes use the same renderer-owned world anchor, so camera
// neutrality must follow that canonical key rather than DOM role.
Expand Down Expand Up @@ -7472,18 +7475,8 @@ export async function applyRenderedWebglResourceOccupantInteraction(

const overviewFramed = focusedClosed && await frameRealmOverview();
let overviewPresence;
let overviewLane = 'presence';
const overviewLane = 'control';
const overviewPresenceReady = overviewFramed && await waitFor(() => {
const passiveCandidate = overviewPreferredKeys
.map((key) => presentationForKey(
'.realm-resource-occupant-presence',
key
))
.find((element) => (
element instanceof HTMLElement
&& element.getAttribute('data-projected-visible') === 'true'
&& visible(element)
));
const controlCandidate = overviewPreferredKeys
.map((key) => presentationForKey(
'button.realm-resource-occupant-marker',
Expand All @@ -7494,13 +7487,11 @@ export async function applyRenderedWebglResourceOccupantInteraction(
&& element.getAttribute('data-projected-visible') === 'true'
&& visible(element)
));
// Compact viewports may truthfully have no collision-safe passive
// portrait after controls, castle labels, and safe areas are reserved.
// Exercise the same canonical record through its single bounded
// control lane instead of requiring an overlapping/clipped duplicate.
const candidate = passiveCandidate ?? controlCandidate;
if (!(candidate instanceof HTMLElement)) return false;
overviewLane = passiveCandidate ? 'presence' : 'control';
// Passive overflow portraits are visual context only. Record activation
// must always use the bounded native-button lane; never fall through to
// an aria-hidden presentation when collision admission changes.
const candidate = controlCandidate;
if (!(candidate instanceof HTMLButtonElement)) return false;
overviewTargetKey = candidate.getAttribute(
'data-resource-occupant-key'
) ?? '';
Expand All @@ -7522,16 +7513,41 @@ export async function applyRenderedWebglResourceOccupantInteraction(
if (overviewProjectionSettled) {
// Collision reconciliation may move the same canonical occupation
// between its passive PFP and single keyboard-control lane while the
// projection settles. Reacquire that lane before measuring or
// activating it so the proof never clicks a detached stale element.
const settledPresentation = overviewPresentation();
if (settledPresentation instanceof HTMLElement) {
overviewPresence = settledPresentation;
overviewLane = settledPresentation.matches(
'.realm-resource-occupant-presence'
) ? 'presence' : 'control';
// projection settles. Reacquire only the control; a passive fallback is
// deliberately not activatable.
const settledControl = presentationForKey(
'button.realm-resource-occupant-marker',
overviewTargetKey
);
if (settledControl instanceof HTMLButtonElement) {
overviewPresence = settledControl;
} else {
overviewPresence = undefined;
}
}
const passivePresence = [...document.querySelectorAll(
'.realm-resource-occupant-presence[data-projected-visible="true"]'
)].find((element) => element instanceof HTMLElement && visible(element));
const passivePresenceBounds = passivePresence instanceof HTMLElement
? passivePresence.getBoundingClientRect()
: undefined;
const passiveDirectHit = passivePresenceBounds
? document.elementFromPoint(
passivePresenceBounds.left + passivePresenceBounds.width / 2,
passivePresenceBounds.top + passivePresenceBounds.height / 2
)
: null;
const passivePresenceVisualOnly = overviewProjectionSettled
&& passivePresence instanceof HTMLElement
&& presenceLayer instanceof HTMLElement
&& getComputedStyle(passivePresence).pointerEvents === 'none'
&& getComputedStyle(passivePresence).cursor !== 'pointer'
&& presenceLayer.getAttribute('aria-hidden') === 'true'
&& !(passiveDirectHit instanceof HTMLElement
&& (
passiveDirectHit === passivePresence
|| passivePresence.contains(passiveDirectHit)
));
const overviewPresenceBounds = overviewPresence instanceof HTMLElement
? overviewPresence.getBoundingClientRect()
: undefined;
Expand Down Expand Up @@ -7623,7 +7639,7 @@ export async function applyRenderedWebglResourceOccupantInteraction(
'.realm-resource-occupant-presence',
overviewTargetKey
);
const overviewTargetPassiveOnly = overviewProjectionSettled
const overviewTargetControlOnly = overviewProjectionSettled
&& overviewPresenceDirectHit
&& (
overviewLane === 'presence'
Expand All @@ -7635,7 +7651,7 @@ export async function applyRenderedWebglResourceOccupantInteraction(
const beforeRenderer = rendererSnapshot();
const beforeProjection = projectionSnapshot();
const overviewPresencePrivacyBounded = subtreePrivacyBounded(overviewPresence);
if (overviewTargetPassiveOnly && overviewDirectHit instanceof HTMLElement) {
if (overviewTargetControlOnly && overviewDirectHit instanceof HTMLElement) {
overviewDirectHit.click();
}
const overviewPanelReady = await waitFor(() => {
Expand Down Expand Up @@ -7702,7 +7718,7 @@ export async function applyRenderedWebglResourceOccupantInteraction(
)
)
&& [...overviewFacts.keys()].some((label) => label.endsWith('time left'));
const presenceDelegatedActivation = overviewTargetPassiveOnly
const overviewControlActivation = overviewTargetControlOnly
&& overviewPanelReady
&& overviewRecordCorrect;
const focusedPrivacyBounded = markerPrivacyBounded
Expand Down Expand Up @@ -7801,11 +7817,12 @@ export async function applyRenderedWebglResourceOccupantInteraction(
markerHitTestable,
overviewPresenceDirectHit,
overviewRecordCorrect,
overviewTargetPassiveOnly,
overviewTargetControlOnly,
passivePresenceVisualOnly,
presenceComputedVisible,
presenceAvatarGeometryValid,
presenceGeometryValid,
presenceDelegatedActivation,
overviewControlActivation,
presenceHitTestable,
presencePointerActivatable,
presencePortraitElementPresent,
Expand Down
19 changes: 17 additions & 2 deletions scripts/verify-alpha-production.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,11 @@ async function verifyAuthV2AdminBrowserIsolation(frontend, bridge, fetchImpl) {
'access-control-request-headers': 'authorization, content-type',
}),
}),
Object.freeze({
name: 'allowed-origin credential-free POST',
method: 'POST',
headers: Object.freeze({ origin: frontend }),
}),
]);

for (const pathname of AUTH_V2_SERVER_ONLY_ADMIN_PATHS) {
Expand All @@ -1187,8 +1192,18 @@ async function verifyAuthV2AdminBrowserIsolation(frontend, bridge, fetchImpl) {
}, fetchImpl);
verifyAuthV2SecurityHeaders(response, label);
verifyNoCors(response, label);
const payload = await readExactJsonAtStatus(response, label, 404);
verifyExactErrorPayload(payload, 'not_found', 'Route not found.', label);
if (check.method === 'POST') {
const payload = await readExactJsonAtStatus(response, label, 403);
verifyExactErrorPayload(
payload,
'admin_browser_forbidden',
'This endpoint is server-only.',
label,
);
} else {
const payload = await readExactJsonAtStatus(response, label, 404);
verifyExactErrorPayload(payload, 'not_found', 'Route not found.', label);
}
}
}
}
Expand Down
Loading