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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ full engineering record.

## [Unreleased]

## [0.3.34] — 2026-07-31

- Made Request Access become a disabled Request Sent action immediately after
the first click, before the private authority response returns.
- Kept a request already recorded in SpacetimeDB visibly received and
non-clickable across later status checks.
- Added rapid-repeat coverage proving one player gesture produces at most one
bridge submission while the server remains cycle-idempotent.

## [0.3.33] — 2026-07-31

- Gave every admitted keeper one experimental Community Mark per eligible
Expand Down Expand Up @@ -371,7 +380,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.33...HEAD
[Unreleased]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.34...HEAD
[0.3.34]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.33...v0.3.34
[0.3.33]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.32...v0.3.33
[0.3.32]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.31...v0.3.32
[0.3.31]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.30...v0.3.31
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.33 keeps backend protocol 3 and authentication contract v2; admission
Alpha 0.3.34 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.33` is the live **The Realm Remembers** release. A version is
Alpha `0.3.34` is the live **The Petition Stands** 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.33",
"version": "0.3.34",
"description": "A Farcaster-connected persistent strategy world in active Alpha development.",
"license": "Apache-2.0",
"homepage": "https://warpkeep.com",
Expand Down
4 changes: 2 additions & 2 deletions src/components/auth/FarcasterAccessRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function FarcasterAccessRequestMessage({
if (state.phase === 'submitting') {
return (
<p className="farcaster-auth-panel__instruction" role="status">
Sending request
<strong>Request sent.</strong> Confirming with the Hegemony records
</p>
);
}
Expand Down Expand Up @@ -102,7 +102,7 @@ export function FarcasterAccessRequestAction({
const label = isLoading
? 'CHECKING…'
: isSubmitting
? 'SENDING…'
? 'REQUEST SENT'
: isRequested
? 'REQUEST RECEIVED'
: 'REQUEST ACCESS';
Expand Down
15 changes: 15 additions & 0 deletions src/components/menu/latestPatchNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ export type LatestPatchNotes = Readonly<{
alphaNotice: string;
}>;

const ALPHA_0_3_34_PATCH_NOTES: LatestPatchNotes = Object.freeze({
releasedOn: '31 JUL 2026',
title: 'THE PETITION STANDS',
summary:
'A keeper’s petition now settles visibly the instant it is sent, while the Realm’s private record remains final.',
highlights: Object.freeze([
'Request Access becomes a disabled Request Sent action on the first click, without waiting for a network round trip.',
'A petition already recorded by SpacetimeDB remains visibly received and cannot be submitted again from the gateway.',
'Rapid repeated gestures are absorbed locally, while the existing private server record remains cycle-idempotent and manually reviewed.'
]),
alphaNotice:
'Alpha 0.3.34 remains an invite-only, unfinished world. Sending a petition does not grant admission, create a keep, or change any Realm ownership or balance.'
});

const ALPHA_0_3_33_PATCH_NOTES: LatestPatchNotes = Object.freeze({
releasedOn: '31 JUL 2026',
title: 'THE REALM REMEMBERS',
Expand Down Expand Up @@ -233,6 +247,7 @@ const ALPHA_0_3_22_PATCH_NOTES: LatestPatchNotes = Object.freeze({

export const WARPKEEP_PATCH_NOTES_BY_VERSION: Readonly<Record<string, LatestPatchNotes>> =
Object.freeze({
'0.3.34': ALPHA_0_3_34_PATCH_NOTES,
'0.3.33': ALPHA_0_3_33_PATCH_NOTES,
'0.3.32': ALPHA_0_3_32_PATCH_NOTES,
'0.3.31': ALPHA_0_3_31_PATCH_NOTES,
Expand Down
4 changes: 2 additions & 2 deletions tests/buildInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ describe('Warpkeep build identity', () => {
);
});

it('reads the 0.3.33 product version from package metadata', () => {
it('reads the 0.3.34 product version from package metadata', () => {
const packageJson = JSON.parse(
readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')
) as { version?: unknown };
const packageLock = JSON.parse(
readFileSync(resolve(process.cwd(), 'package-lock.json'), 'utf8')
) as { version?: unknown; packages?: Record<string, { version?: unknown }> };

expect(readWarpkeepProductVersion(packageJson.version)).toBe('0.3.33');
expect(readWarpkeepProductVersion(packageJson.version)).toBe('0.3.34');
expect(packageLock.version).toBe(packageJson.version);
expect(packageLock.packages?.['']?.version).toBe(packageJson.version);
expect(readWarpkeepProductVersion('1.0.0-alpha.1+build.7')).toBe('1.0.0-alpha.1+build.7');
Expand Down
70 changes: 64 additions & 6 deletions tests/farcasterAccessRequestLifecycle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { FarcasterAccessRequestAction } from '../src/components/auth/FarcasterAccessRequest';
import { useAccessRequest } from '../src/farcaster/useAccessRequest';
import type {
AccessRequestStatus,
Expand All @@ -27,6 +28,14 @@ const pending = (fid: number): FarcasterAuthViewState => Object.freeze({

const anonymous: FarcasterAuthViewState = Object.freeze({ phase: 'anonymous' });

function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((resolvePromise) => {
resolve = resolvePromise;
});
return Object.freeze({ promise, resolve });
}

function bridge(overrides: Partial<FarcasterOidcBridgeClient> = {}): FarcasterOidcBridgeClient {
return {
issuer: 'https://auth.warpkeep.example',
Expand Down Expand Up @@ -76,7 +85,10 @@ function Harness({
<div>
<output>{access.state.phase}</output>
{'requestedAt' in access.state ? <time>{access.state.requestedAt}</time> : null}
<button onClick={access.requestAccess} type="button">request</button>
<FarcasterAccessRequestAction
onRequestAccess={access.requestAccess}
state={access.state}
/>
</div>
);
}
Expand Down Expand Up @@ -173,7 +185,7 @@ describe('access-request controller lifecycle', () => {

await waitFor(() => expect(screen.getByText('not-requested')).not.toBeNull());
expect(requestAccess).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'request' }));
fireEvent.click(screen.getByRole('button', { name: 'REQUEST ACCESS' }));
await waitFor(() => expect(screen.getByText('requested')).not.toBeNull());
expect(requestAccess).toHaveBeenCalledTimes(1);
expect(screen.getByText('1785414896000')).not.toBeNull();
Expand All @@ -194,7 +206,7 @@ describe('access-request controller lifecycle', () => {

await waitFor(() => expect(screen.getByText('error')).not.toBeNull());
expect(requestAccess).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'request' }));
fireEvent.click(screen.getByRole('button', { name: 'REQUEST ACCESS' }));
await waitFor(() => expect(screen.getByText('requested')).not.toBeNull());

expect(getAccessRequestStatus).toHaveBeenCalledTimes(2);
Expand All @@ -212,7 +224,7 @@ describe('access-request controller lifecycle', () => {
render(<Harness authState={pending(12_345)} client={client} generation={1} />);

await waitFor(() => expect(screen.getByText('error')).not.toBeNull());
fireEvent.click(screen.getByRole('button', { name: 'request' }));
fireEvent.click(screen.getByRole('button', { name: 'REQUEST ACCESS' }));
await waitFor(() => expect(screen.getByText('error')).not.toBeNull());

expect(getAccessRequestStatus).toHaveBeenCalledTimes(2);
Expand All @@ -232,7 +244,7 @@ describe('access-request controller lifecycle', () => {
render(<Harness authState={pending(12_345)} client={client} generation={1} />);

await waitFor(() => expect(screen.getByText('error')).not.toBeNull());
fireEvent.click(screen.getByRole('button', { name: 'request' }));
fireEvent.click(screen.getByRole('button', { name: 'REQUEST ACCESS' }));
await waitFor(() => expect(screen.getByText('requested')).not.toBeNull());

expect(getAccessRequestStatus).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -263,7 +275,7 @@ describe('access-request controller lifecycle', () => {
);

await waitFor(() => expect(screen.getByText('not-requested')).not.toBeNull());
fireEvent.click(screen.getByRole('button', { name: 'request' }));
fireEvent.click(screen.getByRole('button', { name: 'REQUEST ACCESS' }));
await waitFor(() => expect(screen.getByText('requested')).not.toBeNull());

expect(requestAccess).toHaveBeenCalledTimes(1);
Expand All @@ -276,4 +288,50 @@ describe('access-request controller lifecycle', () => {
expect(document.documentElement.innerHTML).not.toContain(token);
expect(JSON.stringify(screen.getByText('requested').textContent)).not.toContain(token);
});

it('switches to a disabled sent state immediately and submits only once', async () => {
const submitted = deferred<AccessRequestStatus>();
const requestAccess = vi.fn(() => submitted.promise);
const client = bridge({ requestAccess });
render(<Harness authState={pending(12_345)} client={client} generation={1} />);

await waitFor(() => expect(screen.getByText('not-requested')).not.toBeNull());
const request = screen.getByRole('button', { name: 'REQUEST ACCESS' });
fireEvent.click(request);

expect(screen.getByText('submitting')).not.toBeNull();
const sent = screen.getByRole('button', { name: 'REQUEST SENT' }) as HTMLButtonElement;
expect(sent.disabled).toBe(true);
fireEvent.click(sent);
await waitFor(() => expect(requestAccess).toHaveBeenCalledTimes(1));

submitted.resolve({
version: 1,
status: 'requested',
requestedAt: 1_785_414_896_000
});
await waitFor(() => expect(screen.getByText('requested')).not.toBeNull());
expect(requestAccess).toHaveBeenCalledTimes(1);
});

it('never submits when SpacetimeDB already contains the current request', async () => {
const requestAccess = vi.fn();
const client = bridge({
getAccessRequestStatus: vi.fn(async () => ({
version: 1 as const,
status: 'requested' as const,
requestedAt: 1_785_414_896_000
})),
requestAccess
});
render(<Harness authState={pending(12_345)} client={client} generation={1} />);

await waitFor(() => expect(screen.getByText('requested')).not.toBeNull());
const received = screen.getByRole('button', {
name: 'REQUEST RECEIVED'
}) as HTMLButtonElement;
expect(received.disabled).toBe(true);
fireEvent.click(received);
expect(requestAccess).not.toHaveBeenCalled();
});
});
21 changes: 21 additions & 0 deletions tests/farcasterAdmissionPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ describe('FarcasterAdmissionPanel', () => {
expect(onCheckAgain).toHaveBeenCalledTimes(1);
});

it('renders the local request latch immediately as a disabled sent action', () => {
render(
<FarcasterAdmissionPanel
accessRequest={{ phase: 'submitting' }}
identity={identity}
onBackToMenu={vi.fn()}
onCheckAgain={vi.fn()}
onRequestAccess={vi.fn()}
onSignOut={vi.fn()}
phase="denied"
/>
);

expect(screen.getByText('Request sent.')).not.toBeNull();
expect(screen.getByText(/Confirming with the Hegemony records/i)).not.toBeNull();
expect((screen.getByRole('button', {
name: 'REQUEST SENT'
}) as HTMLButtonElement).disabled).toBe(true);
expect(screen.queryByRole('button', { name: 'CHECK AGAIN' })).toBeNull();
});

it('keeps a backend outage distinct from an admission rejection', () => {
render(
<FarcasterAdmissionPanel
Expand Down
5 changes: 5 additions & 0 deletions tests/farcasterQrAuthPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ describe('FarcasterQrAuthPanel', () => {
accessRequest: { phase: 'submitting' }
});

expect(screen.getByText('Request sent.')).not.toBeNull();
expect((screen.getByRole('button', {
name: 'REQUEST SENT'
}) as HTMLButtonElement).disabled).toBe(true);

panel.rerender(
<FarcasterQrAuthPanel
phase="pending-admission"
Expand Down
26 changes: 20 additions & 6 deletions tests/latestPatchNotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,38 @@ describe('latest in-menu patch notes', () => {
expect(Object.keys(WARPKEEP_PATCH_NOTES_BY_VERSION)).toContain(packageJson.version);
expect(getLatestPatchNotes(packageJson.version)).toMatchObject({
releasedOn: '31 JUL 2026',
title: 'THE REALM REMEMBERS'
title: 'THE PETITION STANDS'
});
expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).toMatch(
/SpacetimeDB.*one exact Mark.*UTC Realm day.*no wallet.*token.*transaction.*payment/i
/Request Access.*disabled Request Sent.*first click.*without waiting/i
);
expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).toMatch(
/Private daily receipts.*retries harmless.*revoked admission.*pauses future grants/i
/already recorded by SpacetimeDB.*received.*cannot be submitted again/i
);
expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).toMatch(
/SNAP burn.*wallet-attribution.*chain-scanning.*retired/i
/repeated gestures.*absorbed locally.*cycle-idempotent.*manually reviewed/i
);
expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).not.toMatch(
/released to players|deployed to players|public balances|guaranteed rewards/i
);
expect(getLatestPatchNotes(packageJson.version)?.summary).toContain('Community Mark');
expect(getLatestPatchNotes(packageJson.version)?.summary).toContain('petition');
expect(getLatestPatchNotes(packageJson.version)?.alphaNotice).toContain('unfinished');
expect(getLatestPatchNotes(packageJson.version)?.alphaNotice).toContain(
'cannot be spent, transferred, redeemed, or converted'
'does not grant admission'
);

const alpha033 = getLatestPatchNotes('0.3.33');
expect(alpha033).toMatchObject({
title: 'THE REALM REMEMBERS'
});
expect(alpha033?.highlights.join(' ')).toMatch(
/SpacetimeDB.*one exact Mark.*UTC Realm day.*no wallet.*token.*transaction.*payment/i
);
expect(alpha033?.highlights.join(' ')).toMatch(
/Private daily receipts.*retries harmless.*revoked admission.*pauses future grants/i
);
expect(alpha033?.highlights.join(' ')).toMatch(
/SNAP burn.*wallet-attribution.*chain-scanning.*retired/i
);

const alpha0324 = getLatestPatchNotes('0.3.24');
Expand Down
2 changes: 1 addition & 1 deletion tests/menuFarcasterAuthIntegration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ describe('WarpkeepMainMenu Farcaster authentication integration', () => {
'CREDITS'
]);
expect(screen.getByRole('button', {
name: 'Open patch notes for Warpkeep ALPHA 0.3.33'
name: 'Open patch notes for Warpkeep ALPHA 0.3.34'
})).not.toBeNull();

expect(screen.queryByRole('button', { name: 'CONTINUE' })).toBeNull();
Expand Down
Loading