diff --git a/CHANGELOG.md b/CHANGELOG.md index 303cef82..c7758c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ full engineering record. ## [Unreleased] +- Drafted the next Alpha Terms, Hegemony Social Contract, and Privacy Notice + for a future persistent Realm Chat, including explicit conduct, reporting, + moderation, history, and privacy boundaries. Chat remains disabled pending + owner and qualified legal review, server authority, release gates, and a + separate activation record; this draft collects no chat data. +- Blocked independent production deployment of the review-only agreement on + both Pages and SpacetimeDB, pinned the proposed Privacy Notice text digest, + and removed duplicated agreement-version literals from migration and local + full-stack QA so rollout drift fails closed. - Prepared an optional **Enable Admission Alerts** step after a confirmed access request. It remains fail-closed behind a default-off release gate until the owner-controlled signed notification canary is complete, and never affects diff --git a/docs/design/realm-chat-v1-contract.md b/docs/design/realm-chat-v1-contract.md new file mode 100644 index 00000000..53d8df52 --- /dev/null +++ b/docs/design/realm-chat-v1-contract.md @@ -0,0 +1,163 @@ +# Realm Chat V1 contract + +Status: **proposal only; disabled** + +Policy version: `2026-08-03-realm-chat-policy-v1` + +Realm channel: `realm:genesis-001` + +This document fixes the product, authority, privacy, and moderation boundaries +for the first Warpkeep chat implementation. It does not activate chat, publish a +database, seed a channel, or authorize a client entry point. + +## Release gates + +Realm Chat must remain unavailable until all of these are complete: + +1. The project owner and a qualified legal reviewer approve the proposed Terms, + Social Contract, Privacy Notice, persistence language, moderation process, + and an explicit age/minor-participation policy. +2. A later additive SpacetimeDB PR implements and verifies the private archive, + caller-bounded views, reducers, indexes, and report records. +3. A later client PR implements the portrait and desktop experiences without + creating browser-side identity, sequence, or time authority. +4. Operator moderation procedures, evidence handling, release checks, and a + tested kill switch exist. +5. A separate activation record names the reviewed versions, exact deployment, + seeded channel, canary evidence, rollback owner, and timestamp. + +Merging this contract alone must not make a chat control visible or callable. +The client-side `false` constant is documentary defense in depth, not an +activation mechanism. Server channel state remains authoritative. + +The proposed V5 agreement is also marked `review-only-rollout-blocked`. +Warpkeep's production Pages validator and canonical SpacetimeDB publisher must +reject deployment/publication under that status. Read-only publisher dry runs +remain available for compatibility evidence. A later reviewed rollout may change it to +`production-approved` only when the exact legal approval and a coordinated +browser/module compatibility proof are recorded. This prevents an automatic +V5 Pages deployment from meeting a still-V4 production module and failing every +player's entry-agreement handshake. + +## V1 product boundary + +- One admitted-Realm channel only: `realm:genesis-001`. +- No direct messages, guild channels, trading chat, links with rich previews, + attachments, voice, or cross-Realm federation. +- Mobile uses a full-screen portrait surface. Desktop uses a bounded dock. +- Recent messages are live; older retained messages are paginated. +- A player can locally mute another sender for the current browser session. + Local mute is not a server punishment and does not alter other players' view. +- A report attaches to one exact message and preserves its relevant context for + private review. A report never triggers automatic punishment. + +Comparable games commonly separate world/guild audiences and provide reporting +from the relevant player or message surface. See the official +[Forge of Empires chat overview](https://support.innogames.com/kb/ForgeOfEmpires/en_DK/963), +[Forge of Empires reporting flow](https://support.innogames.com/kb/ForgeOfEmpires/en_DK/964), +[Travian messaging overview](https://support.travian.com/en/articles/11-interacting-with-other-players), +and [Travian report guidance](https://support.travian.com/en/articles/121-i-think-a-player-is-violating-game-rules-what-can-i-do). +Warpkeep V1 deliberately starts with fewer channel types. + +## Server authority and visibility + +The browser supplies only intended message text and an operation request. The +server derives the admitted sender FID, public-profile reference, channel, +sequence, authoritative time, and any visibility state. It validates the exact +current entry agreement and active channel before accepting a message. + +The future permanent message archive must be private. Clients must not be able +to subscribe to or enumerate the full archive, report records, moderator notes, +or internal enforcement state. A bounded recent projection may expose up to 128 +permitted messages. A caller-specific paginated history view may expose at most +50 permitted messages per request and must use indexed lookups. SpacetimeDB +documents that private tables are unavailable to clients and that views can +filter private rows by caller; that is the required later authority pattern: +[table access permissions](https://spacetimedb.com/docs/tables/access-permissions/) +and [views](https://spacetimedb.com/docs/functions/views/). + +Messages form persistent Realm history and have no routine gameplay expiry. +That is not a promise of immutable public display or universal physical +retention. Authorized moderation, safety, privacy, legal, service-integrity, or +Realm-reset work may restrict, tombstone, anonymise, or erase a record. Provider +backup lifecycles may differ from active database state. + +## Candidate limits requiring owner review + +The following are implementation candidates, not approved live limits: + +- 500 Unicode scalar values; +- 2,048 UTF-8 bytes; +- 8 lines; +- 2 seconds between accepted messages; +- 10 accepted messages per rolling minute; +- 60 accepted messages per rolling hour; and +- rejection of the same normalized body from one sender within 60 seconds. + +The authority PR must define normalization, Unicode handling, counting windows, +retry responses, and adversarial tests before these numbers become enforceable. + +## Conduct and moderation + +Good-faith criticism of Warpkeep, its maintainer, rules, or features is allowed. +The project may still restrict disruptive conduct contextually, including +political or controversial discussion that overwhelms the game's shared space. +The maintainer exercises broad good-faith judgment but does not claim to make a +definitive legal determination. + +High-risk categories include credible threats or incitement; doxxing, stalking, +or targeted harassment; sexual exploitation or child sexual abuse material; +terrorism or instructions for serious harm; fraud, phishing, malware, account +compromise, or illegal trade; non-consensual intimate content; unlawful hate or +discriminatory abuse; and attempts to obtain or publish non-public personal or +authentication data. + +A warning is not required or guaranteed. Internal reasons remain private. Where +safe and applicable, the affected player should receive a brief understandable +notice that does not expose reporters, personal data, security methods, or an +active investigation. The Alpha does not promise a formal appeal system; it +offers a private reconsideration/legal-contact route without limiting rights +available under applicable law. Knowingly false, retaliatory, or abusive reports +may themselves affect access. Security research reported through the repository +[Security Policy](https://github.com/ael-dev3/Warpkeep/security/policy) remains +protected and distinct from ordinary chat moderation. + +## Privacy boundary + +If activated, the feature processes message body, verified FID, public profile +link, server time and sequence, recipients or visibility scope, report data, and +private moderator decisions. It uses these records to deliver shared Realm +communication, prevent abuse, investigate reports, protect the service, enforce +the agreement, and meet applicable legal obligations. It does not sell them or +use them for advertising. + +The Privacy Notice must identify providers and processing locations, retention +and exceptions, lawful bases where applicable, and available access, +rectification, erasure, restriction, objection, portability, and complaint +rights. The EDPB's small-business guidance emphasizes an identified legal basis, +data minimisation, transparent purposes, security, storage limits, and procedures +for individual rights: [data protection basics](https://www.edpb.europa.eu/sme/learn-the-basics/data-protection-basics_en), +[lawful processing](https://www.edpb.europa.eu/sme/be-compliant/process-personal-data-lawfully_en), +and [individual rights](https://www.edpb.europa.eu/sme/be-compliant/respect-individuals-rights_ga). + +The exact normalized visible Privacy Notice text is integrity-pinned alongside +the Terms and Social Contract even though the notice is not treated as blanket +consent. + +The age/minor-participation policy is intentionally unresolved. This contract +sets no age threshold. Activation is blocked until the owner and qualified legal +reviewer approve the applicable policy and any required parent/guardian, notice, +consent, or access measures. + +## Planned PR sequence + +1. Legal/product contract (this PR; disabled). +2. Additive SpacetimeDB authority and generated bindings. +3. Client portrait/mobile and desktop-dock experience. +4. Desktop/accessibility/abuse QA. +5. Operator moderation and release integration. +6. Separate, evidence-backed activation record. + +Each PR must remain independently reviewable. No implementation PR may weaken +the legal gate, expose the permanent archive, or collapse private moderation +records into public game state. diff --git a/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md b/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md new file mode 100644 index 00000000..23637f6c --- /dev/null +++ b/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md @@ -0,0 +1,63 @@ +# Proposed Realm Chat entry-agreement V5 + +Status: **owner and qualified legal review required; do not activate chat** + +Entry-agreement bundle `2026-08-03-hegemony-entry-agreement-v5` proposes these +exact public documents: + +- Alpha Terms revision `2026-08-03-v5`; +- Hegemony Social Contract version + `2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4`; and +- Privacy Notice revision `2026-08-03-v6` (notice only, not blanket consent). + +Normalized visible `
` text digests proposed for review: + +- Terms: `dede7757c3be767b7a87e89e2c68817e9390cde91fabcf38246756afacdf51bd`; +- Social Contract: `85941d066dd39f5be069d640f1419491e6fc0f691d01c292bfc3ed995c249110`; +- Privacy Notice: `79bd17b795b399391ed0f6f84f2c7ff35fdb3ae64bf4ca17e3df0ad67d7b361f`. + +The proposed agreement explains a future persistent Realm Chat, prohibited +high-risk conduct, contextual moderation, good-faith criticism, reports, local +mute, reconsideration, and the unresolved age/minor-participation policy. The +Privacy Notice conditionally discloses the message, identity, recipient, +reporting, moderation, provider, retention, and individual-rights data flows. + +## Consequence of approval + +The Terms and Social Contract wording changes materially. If this bundle is +approved, merged, and deployed as the current agreement, every Keeper must make +a fresh unchecked acceptance before authenticated entry. Earlier acceptance +evidence remains immutable historical evidence and cannot satisfy the V5 entry +gate. + +The bundle change does not itself activate chat. This PR intentionally contains +no chat table, reducer, binding, UI, seeded channel, database publication, +operator action, or production-state claim. `WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED` +remains `false`. Chat activation requires the later PR sequence and a separate +activation record described in +[`docs/design/realm-chat-v1-contract.md`](../design/realm-chat-v1-contract.md). + +The candidate bundle remains `review-only-rollout-blocked`. Both the production +Pages validator and canonical SpacetimeDB publisher must fail closed on live +deployment/publication under that status; read-only publisher dry runs remain +available for compatibility evidence. Promotion to `production-approved` requires a later reviewed change +that records the exact approval and proves a coordinated browser/module rollout; +otherwise V5 browser code could auto-deploy against a still-V4 module and deny +Realm entry to every player. + +## Required approval record + +Before merge, the owner and qualified legal reviewer must explicitly resolve +and approve: + +- exact version identifiers and final visible wording; +- the age/minor-participation policy; +- persistence, deletion, tombstone, anonymisation, and backup language; +- prohibited-content and good-faith-discretion language; +- report handling, moderator notice, reconsideration, and evidence access; +- privacy purposes, lawful bases, recipients, locations, retention, and rights; + and +- a private legal/privacy contact path suitable for production use. + +This repository record is project-authored engineering documentation, not a +legal-compliance certification or substitute for qualified legal advice. diff --git a/public/privacy/index.html b/public/privacy/index.html index 545e87c4..04f4e180 100644 --- a/public/privacy/index.html +++ b/public/privacy/index.html @@ -30,7 +30,7 @@

Privacy Notice

why it is used, where it goes, and the limits of retention and deletion.

3. Data, sources, and purposes

authenticates, and requests entry, SpacetimeDB stores private, immutable evidence containing the verified FID, exact entry-agreement bundle version, and acceptance time. The Alpha Terms incorporate Hegemony Social Contract version - 2026-07-19-HEGEMONY-SOCIAL-CONTRACT-V3 in entry-agreement bundle - 2026-07-31-hegemony-entry-agreement-v4; the published browser policy + 2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4 in entry-agreement bundle + 2026-08-03-hegemony-entry-agreement-v5; the published browser policy cryptographically binds the exact visible Terms and Social Contract texts to their reviewed hashes. This evidence is used to enforce the current entry agreement and preserve an audit trail. It is not a public projection and does not contain the @@ -184,6 +184,20 @@

3. Data, sources, and purposes

units: non-transferable, non-redeemable, without cash value, and subject to correction or reset. +
  • + Realm Chat, if activated: Realm Chat is not active in this release. + It must remain disabled until the owner and legal review gates are complete. If it + is later activated, SpacetimeDB will process the message body, channel, server-owned + sequence and time, the sender's verified FID and public profile link, intended + recipients or visibility scope, message reports, and private moderator decisions. + The browser will not be trusted to choose the sender FID, authoritative time, or + sequence. Other admitted players may receive recent Realm messages and the sender's + public Farcaster presentation. A caller may request their permitted retained + history; the permanent archive, report records, and moderation reasons will not be + exposed as one public subscribable table. A local mute will exist only in the + muting player's current browser session and will not change server records or what + other players can see. +
  • Operations: Warpkeep emits a closed list of generic security and availability event names. Application logs are designed not to contain FIDs, @@ -286,6 +300,10 @@

    5. Retention and deletion limits

    Daily Mark receipts and Mark account No fixed Alpha deletion schedule yet. Private daily-grant receipts and balances may remain for replay protection, correction, security, and audit. Only privacy-bounded aggregate figures become public game state. + + Realm Chat messages, reports, and moderation records, if activated + Realm Chat is currently disabled and does not yet collect these records. If activated after review, messages are intended to form persistent Realm history without routine gameplay expiry. They may still be removed from ordinary player view, restricted, tombstoned, anonymised, or erased for moderation, safety, privacy, legal obligations, service integrity, or a Realm reset. Reports and moderator decisions remain private and may be retained while needed to investigate abuse, enforce rules, resolve disputes, and meet legal obligations. Provider backup lifecycles may differ from active records. + @@ -313,6 +331,13 @@

    6. Services, recipients, and locations

    that host still receives ordinary request data such as the connecting IP. These independent services handle data under their own terms and privacy notices.

    +

    + If Realm Chat is activated, admitted recipients receive the permitted live and + historical message views described above, and authorized moderators may receive the + exact reported message, report context, and related private records. Reporters do not + receive another player's private moderation outcome. Warpkeep does not sell chat or + report data and does not use it for advertising. +

    These are global online services, so information may be processed in multiple countries, including outside the country where you live or access the Alpha. The @@ -338,6 +363,12 @@

    7. Your choices and rights

    you live, work, or where you believe an infringement occurred, when applicable. Contacting the project first is welcome but does not remove any complaint right.

    +

    + Realm Chat's age and minor-participation policy is unresolved. The project must not + activate it until the owner and qualified legal reviewer approve an age policy and + any required parental, guardian, notice, consent, or access measures. No age threshold + is asserted by this draft. +

    diff --git a/public/social-contract/index.html b/public/social-contract/index.html index 67e83639..c2ec0061 100644 --- a/public/social-contract/index.html +++ b/public/social-contract/index.html @@ -27,8 +27,8 @@

    The Imperial Charter of Admission

    Article I · The Realm endures beyond any citizen

    Article II · War stays inside the Realm

    Rivalry and hard strategy belong to the fiction. The person behind every keep does - not. Threats, doxxing, stalking, fraud, malware, account compromise, targeted - harassment, abusive automation, and attempts to obtain another person's non-public - data are forbidden. Follow applicable law and respect boundaries outside the game. + not. Do not post or facilitate credible threats or incitement, doxxing, stalking, + targeted harassment, sexual exploitation or child sexual abuse material, terrorism + or instructions for serious harm, fraud, phishing, malware, account compromise, + illegal trade, non-consensual intimate content, unlawful hate or discriminatory + abuse, or attempts to obtain or publish non-public personal or authentication data. + Follow applicable law and respect boundaries outside the game.

    Article III · Access follows conduct

    - Warpkeep may warn, limit, suspend, or revoke official Alpha access when observable - conduct violates this Charter, the Alpha Terms, applicable law, or the safety and - integrity of the service. Context and severity matter. This standard does not police - lawful opinions, identities, beliefs, or life outside Warpkeep. + Warpkeep may warn, remove or restrict content, limit chat, suspend, or revoke official + Alpha access when conduct violates this Charter, the Alpha Terms, applicable law, or + the safety and integrity of the service. A warning is not required or guaranteed. + The maintainer may exercise broad, good-faith, contextual judgment; this is not a + claim that the maintainer makes definitive legal determinations. Context, severity, + repetition, and credible risk matter. Politics and other controversial subjects may + be restricted when they become disruptive even if the underlying viewpoint is not + clearly unlawful. This standard does not target lawful identities or beliefs.

    @@ -85,21 +92,43 @@

    Article IV · Honest counsel strengthens the Realm

    Article V · The world is still being forged

    The current Alpha has no complete core strategy loop, alliance system, combat system, - or in-game chat. Lore and design drafts are direction, not hidden mechanics, paid - power, rewards, punishments, or future entitlements. A gameplay system becomes real - only when its rules and server authority are implemented and published. + or active in-game chat. Realm Chat is a disabled proposal pending owner and legal + review. If later activated, one Realm channel will let admitted Keepers speak in the + shared world. Messages may persist as Realm history; local muting affects only the + muting player's current session. Reporting must preserve the exact reported message + and context for private review. Reports do not automatically punish anyone, and + knowingly false, retaliatory, or abusive reports may themselves affect access. + Lore and design drafts are direction, not hidden mechanics, paid power, rewards, + punishments, or future entitlements. A gameplay system becomes real only when its + rules, privacy notice, server authority, and activation state are implemented and + published. +

    + + +
    +

    Article VI · Moderation is contextual and private

    +

    + Moderation decisions and internal reasons are private. Where safe and applicable, + an affected player should receive a brief, understandable notice, but Warpkeep does + not promise disclosure that would expose reporters, security methods, personal data, + or an active safety investigation. The Alpha does not promise a formal appeal system. + A player may request reconsideration through the project's private contact route and + keeps any rights available under applicable law.

    -

    Article VI · Changes and contact

    +

    Article VII · Changes and contact

    This exact Charter is incorporated into entry-agreement bundle - 2026-07-31-hegemony-entry-agreement-v4. Material changes require a + 2026-08-03-hegemony-entry-agreement-v5. Material changes require a later visible version and a new unchecked acceptance; earlier evidence never authorizes a later bundle. Public questions may be raised through the Warpkeep repository - without posting private account, authentication, wallet, or moderation evidence. + without posting private account, authentication, wallet, report, or moderation + evidence. For a private reconsideration or legal-contact request, open a + content-free issue asking for a private contact channel; do not place evidence in + the issue. Security concerns belong in the repository's Security Policy.

    diff --git a/public/terms/index.html b/public/terms/index.html index 4cd992e5..59e711ff 100644 --- a/public/terms/index.html +++ b/public/terms/index.html @@ -29,8 +29,8 @@

    Alpha Terms

    These terms apply when you choose to enter and test Warpkeep.

    3. Your entry agreement

    Checking the in-game box and selecting Continue to sign-in means you agree to these Alpha Terms and the Hegemony Social Contract, version - 2026-07-19-HEGEMONY-SOCIAL-CONTRACT-V3. Together they form entry-agreement - bundle 2026-07-31-hegemony-entry-agreement-v4. The checkbox does not + 2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4. Together they form entry-agreement + bundle 2026-08-03-hegemony-entry-agreement-v5. The checkbox does not accept the Privacy Notice as blanket privacy consent.

    @@ -105,6 +105,19 @@

    4. Accounts, access, and conduct

    impersonate others, exploit vulnerabilities for advantage, automate abusive traffic, disrupt the service, or access another player's non-public data.

    +

    + If Realm Chat is later activated, your messages may be visible to other admitted + players and retained as Realm history. Chat access is conditional on the Social + Contract and may be restricted or revoked to protect people, the service, or the + shared Realm. A warning is not required or guaranteed before action. Good-faith + criticism of Warpkeep, its maintainer, or its rules is permitted. +

    +

    + The Alpha's age and minor-participation policy is not final. Realm Chat must remain + unavailable until the project owner and qualified legal reviewer approve that policy + and the related conduct, privacy, persistence, and moderation terms. Do not enter or + use a social feature if applicable law or a parent or guardian does not permit it. +

    @@ -112,7 +125,8 @@

    5. Privacy and availability

    The Privacy Notice describes authentication, public game projections, private authority data, providers, retention limits, and - privacy choices. Some public Realm state intentionally links an admitted FID and + privacy choices, including the additional records that a future Realm Chat would + create. Some public Realm state intentionally links an admitted FID and trusted public Farcaster presentation to a castle. Private balances, daily-grant receipts, and ownership authority remain private; an aggregate Mark balance may be included in the player's enabled public community projection. diff --git a/scripts/entry-agreement-policy.d.mts b/scripts/entry-agreement-policy.d.mts index d47c5fbf..f2d904ea 100644 --- a/scripts/entry-agreement-policy.d.mts +++ b/scripts/entry-agreement-policy.d.mts @@ -1,16 +1,20 @@ export const WARPKEEP_ENTRY_AGREEMENT_VERSION: - '2026-07-31-hegemony-entry-agreement-v4'; + '2026-08-03-hegemony-entry-agreement-v5'; +export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS: + 'review-only-rollout-blocked'; export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS: readonly [ + '2026-07-31-hegemony-entry-agreement-v4', '2026-07-19-hegemony-entry-agreement-v3', '2026-07-19-hegemony-entry-agreement-v2', '2026-07-18-hegemony-entry-agreement-v1', '2026-07-14', ]; export const WARPKEEP_ENTRY_AGREEMENT_EVIDENCE_VERSIONS: readonly [ + '2026-08-03-hegemony-entry-agreement-v5', '2026-07-31-hegemony-entry-agreement-v4', '2026-07-19-hegemony-entry-agreement-v3', '2026-07-19-hegemony-entry-agreement-v2', '2026-07-18-hegemony-entry-agreement-v1', '2026-07-14', ]; -export const WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM: 5; +export const WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM: 6; diff --git a/scripts/entry-agreement-policy.mjs b/scripts/entry-agreement-policy.mjs index fb98351b..680b450b 100644 --- a/scripts/entry-agreement-policy.mjs +++ b/scripts/entry-agreement-policy.mjs @@ -4,9 +4,13 @@ * policy because Node cannot import the SpacetimeDB TypeScript module directly. */ export const WARPKEEP_ENTRY_AGREEMENT_VERSION = - '2026-07-31-hegemony-entry-agreement-v4'; + '2026-08-03-hegemony-entry-agreement-v5'; + +export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS = + 'review-only-rollout-blocked'; export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS = Object.freeze([ + '2026-07-31-hegemony-entry-agreement-v4', '2026-07-19-hegemony-entry-agreement-v3', '2026-07-19-hegemony-entry-agreement-v2', '2026-07-18-hegemony-entry-agreement-v1', diff --git a/scripts/publish-spacetime-dev.d.mts b/scripts/publish-spacetime-dev.d.mts index 11cdac56..5b5438f4 100644 --- a/scripts/publish-spacetime-dev.d.mts +++ b/scripts/publish-spacetime-dev.d.mts @@ -53,6 +53,10 @@ export function readFoundedPublishExpectations( source?: Record, ): Readonly; export function requireCanonicalPublishCoordinates(...args: any[]): any; +export function requireEntryAgreementProductionRelease( + releaseStatus?: string, + dryRun?: boolean, +): void; export function runCurrentAdditiveMigrationProof(...args: any[]): any; export function validateIssuerDeployment(...args: any[]): any; export function verifyCanonicalDatabaseList(...args: any[]): any; diff --git a/scripts/publish-spacetime-dev.mjs b/scripts/publish-spacetime-dev.mjs index 766b0229..815ef43f 100644 --- a/scripts/publish-spacetime-dev.mjs +++ b/scripts/publish-spacetime-dev.mjs @@ -36,6 +36,7 @@ import { } from './spacetime-table-schema-attestation.mjs'; import { WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM, + WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS, } from './entry-agreement-policy.mjs'; import { attestPinnedSpacetimeCli, @@ -1206,6 +1207,19 @@ export function requireCanonicalPublishCoordinates(source = process.env) { } } +export function requireEntryAgreementProductionRelease( + releaseStatus = WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS, + dryRun = false, +) { + if (dryRun === true) return; + if (releaseStatus !== 'production-approved') { + fail( + 'The current entry agreement is review-only; coordinated Pages and ' + + 'SpacetimeDB rollout approval is required before production publication.', + ); + } +} + function validateFoundedPublishExpectations(value) { if ( value === null @@ -3937,6 +3951,10 @@ async function main() { fail(`Set WARPKEEP_PUBLISH_CONFIRM=${database} after reviewing the target database; publish was not attempted.`); } const foundedExpectations = readFoundedPublishExpectations(); + requireEntryAgreementProductionRelease( + WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS, + dryRun, + ); // Remove the Hermes credential from the ambient environment before the // long-running proof spawns any children. The bounded aggregate helpers // receive it only through stdin and every child environment stays allowlisted. diff --git a/scripts/qa-observer/local-fullstack-browser-probe.mjs b/scripts/qa-observer/local-fullstack-browser-probe.mjs index 94aecdd2..3f3c6d14 100644 --- a/scripts/qa-observer/local-fullstack-browser-probe.mjs +++ b/scripts/qa-observer/local-fullstack-browser-probe.mjs @@ -3,6 +3,9 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; +import { + WARPKEEP_ENTRY_AGREEMENT_VERSION, +} from '../entry-agreement-policy.mjs'; import { DevtoolsPipeSession, analyzeRenderedWebglPngScreenshot, @@ -5209,7 +5212,7 @@ export async function runLocalFullstackBrowserProbe(options = {}) { || database.seedAttestation.workerCount !== 28 || database.seedAttestation.entryAgreementAcceptedCurrent !== true || database.seedAttestation.entryAgreementRequiredVersion - !== '2026-07-31-hegemony-entry-agreement-v4' + !== WARPKEEP_ENTRY_AGREEMENT_VERSION || database.seedAttestation.genericAssignments !== 0 || database.seedAttestation.genericOccupations !== 0 || database.seedAttestation.genericSchedules !== 0 diff --git a/scripts/qa-observer/local-fullstack-spacetime.mjs b/scripts/qa-observer/local-fullstack-spacetime.mjs index 8831bb65..6d1ec09e 100644 --- a/scripts/qa-observer/local-fullstack-spacetime.mjs +++ b/scripts/qa-observer/local-fullstack-spacetime.mjs @@ -19,6 +19,9 @@ import { tmpdir } from 'node:os'; import { dirname, isAbsolute, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + WARPKEEP_ENTRY_AGREEMENT_VERSION, +} from '../entry-agreement-policy.mjs'; import { attestPinnedSpacetimeCli } from '../spacetime-cli-attestation.mjs'; export const LOCAL_FULLSTACK_DATABASE = 'warpkeep-local-fullstack'; @@ -39,7 +42,6 @@ const SERVER_STOP_TIMEOUT_MILLISECONDS = 5_000; const PROFILE_POLICY_VERSION = 'trusted-snapchain-profile-v3'; const RESOURCE_POLICY_VERSION = 'genesis-resource-yield-v1'; const WORKER_PROTOCOL_CAPABILITY = 'generic-castle-workers-v1'; -const ENTRY_AGREEMENT_VERSION = '2026-07-31-hegemony-entry-agreement-v4'; const LOCAL_FULLSTACK_FOUNDERS = Object.freeze(Array.from( { length: LOCAL_FULLSTACK_FOUNDER_COUNT }, (_, index) => Object.freeze({ @@ -777,7 +779,7 @@ async function seedLocalRealm(server, privateKey, moduleDigest) { } await callPlayer('bootstrap_player_v2'); await callPlayer('accept_alpha_terms_v1', JSON.stringify([ - ENTRY_AGREEMENT_VERSION, + WARPKEEP_ENTRY_AGREEMENT_VERSION, true, ])); const currentEntryAgreement = JSON.parse( @@ -786,7 +788,7 @@ async function seedLocalRealm(server, privateKey, moduleDigest) { if ( !Array.isArray(currentEntryAgreement) || currentEntryAgreement.length !== 2 - || currentEntryAgreement[0] !== ENTRY_AGREEMENT_VERSION + || currentEntryAgreement[0] !== WARPKEEP_ENTRY_AGREEMENT_VERSION || currentEntryAgreement[1] !== true ) { fail('Disposable founder did not retain exact-current entry agreement authority.'); diff --git a/scripts/validate-pages-deploy-config.mjs b/scripts/validate-pages-deploy-config.mjs index 7f928dd1..6647b750 100644 --- a/scripts/validate-pages-deploy-config.mjs +++ b/scripts/validate-pages-deploy-config.mjs @@ -1,3 +1,10 @@ +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { + WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS, +} from './entry-agreement-policy.mjs'; + const EXPECTED_CANONICAL_ORIGIN = 'https://warpkeep.com'; const EXPECTED_REPOSITORY_URL = 'https://github.com/ael-dev3/Warpkeep'; const EXPECTED_AUDIENCE = 'warpkeep-spacetimedb'; @@ -44,62 +51,78 @@ function exactBoolean(value, label) { fail(`${label} must be exactly true or false.`); } -function main() { - if (process.env.DEPLOY_BASE !== '/') { +export function validatePagesDeploymentConfiguration( + environment = process.env, + options = {}, +) { + const entryAgreementReleaseStatus = + options.entryAgreementReleaseStatus + ?? WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS; + if (entryAgreementReleaseStatus !== 'production-approved') { + fail( + 'the current entry agreement is review-only; coordinated Pages and ' + + 'SpacetimeDB rollout approval is required.', + ); + } + if (environment.DEPLOY_BASE !== '/') { fail('DEPLOY_BASE must be /.'); } - if (process.env.VITE_WARPKEEP_RELEASE_CHANNEL !== 'alpha') { + if (environment.VITE_WARPKEEP_RELEASE_CHANNEL !== 'alpha') { fail('VITE_WARPKEEP_RELEASE_CHANNEL must be alpha.'); } - if (!SHA_PATTERN.test(process.env.VITE_WARPKEEP_BUILD_SHA ?? '')) { + if (!SHA_PATTERN.test(environment.VITE_WARPKEEP_BUILD_SHA ?? '')) { fail('VITE_WARPKEEP_BUILD_SHA must be the full Git commit SHA.'); } - if (process.env.VITE_WARPKEEP_REPOSITORY_URL !== EXPECTED_REPOSITORY_URL) { + if (environment.VITE_WARPKEEP_REPOSITORY_URL !== EXPECTED_REPOSITORY_URL) { fail('VITE_WARPKEEP_REPOSITORY_URL must identify the Warpkeep repository.'); } - if (process.env.VITE_WARPKEEP_CANONICAL_ORIGIN !== EXPECTED_CANONICAL_ORIGIN) { + if (environment.VITE_WARPKEEP_CANONICAL_ORIGIN !== EXPECTED_CANONICAL_ORIGIN) { fail('VITE_WARPKEEP_CANONICAL_ORIGIN must be https://warpkeep.com.'); } const sharedAlphaEnabled = exactBoolean( - process.env.VITE_WARPKEEP_SHARED_ALPHA_ENABLED, + environment.VITE_WARPKEEP_SHARED_ALPHA_ENABLED, 'VITE_WARPKEEP_SHARED_ALPHA_ENABLED' ); exactBoolean( - process.env.VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED, + environment.VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED, 'VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED' ); - if (process.env.VITE_WARPKEEP_OIDC_AUDIENCE !== EXPECTED_AUDIENCE) { + if (environment.VITE_WARPKEEP_OIDC_AUDIENCE !== EXPECTED_AUDIENCE) { fail(`VITE_WARPKEEP_OIDC_AUDIENCE must be ${EXPECTED_AUDIENCE}.`); } - if (process.env.VITE_SPACETIMEDB_URI !== EXPECTED_SPACETIMEDB_URI) { + if (environment.VITE_SPACETIMEDB_URI !== EXPECTED_SPACETIMEDB_URI) { fail(`VITE_SPACETIMEDB_URI must be ${EXPECTED_SPACETIMEDB_URI}.`); } - if (process.env.VITE_SPACETIMEDB_DATABASE !== EXPECTED_SPACETIMEDB_DATABASE) { + if (environment.VITE_SPACETIMEDB_DATABASE !== EXPECTED_SPACETIMEDB_DATABASE) { fail(`VITE_SPACETIMEDB_DATABASE must be ${EXPECTED_SPACETIMEDB_DATABASE}.`); } if (!sharedAlphaEnabled) { - console.log('Pages deployment validation passed with shared alpha disabled.'); - return; + return 'Pages deployment validation passed with shared alpha disabled.'; } const bridge = exactHttpsOrigin( - process.env.VITE_WARPKEEP_AUTH_BRIDGE_URL, + environment.VITE_WARPKEEP_AUTH_BRIDGE_URL, 'VITE_WARPKEEP_AUTH_BRIDGE_URL' ); const issuer = exactHttpsOrigin( - process.env.VITE_WARPKEEP_OIDC_ISSUER, + environment.VITE_WARPKEEP_OIDC_ISSUER, 'VITE_WARPKEEP_OIDC_ISSUER' ); if (bridge !== EXPECTED_BRIDGE || issuer !== EXPECTED_BRIDGE) { fail(`the bridge URL and OIDC issuer must both be ${EXPECTED_BRIDGE}.`); } - console.log('Pages deployment validation passed with shared alpha enabled.'); + return 'Pages deployment validation passed with shared alpha enabled.'; } -try { - main(); -} catch (error) { - console.error(error instanceof Error ? error.message : 'Pages deployment configuration is invalid.'); - process.exitCode = 1; +const isEntrypoint = typeof process.argv[1] === 'string' + && import.meta.url === pathToFileURL(resolve(process.argv[1])).href; + +if (isEntrypoint) { + try { + console.log(validatePagesDeploymentConfiguration()); + } catch (error) { + console.error(error instanceof Error ? error.message : 'Pages deployment configuration is invalid.'); + process.exitCode = 1; + } } diff --git a/scripts/verify-spacetime-additive-migration.mjs b/scripts/verify-spacetime-additive-migration.mjs index 94c7eb54..3816d3c2 100644 --- a/scripts/verify-spacetime-additive-migration.mjs +++ b/scripts/verify-spacetime-additive-migration.mjs @@ -21,6 +21,10 @@ import { import { canonicalTableSchemaBoundaryDigest, } from './spacetime-table-schema-attestation.mjs'; +import { + WARPKEEP_ENTRY_AGREEMENT_VERSION, + WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS, +} from './entry-agreement-policy.mjs'; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const fixtureModule = resolve( @@ -103,13 +107,9 @@ const actualModuleOtherFid = 730_002; // and is used only to prove the private v13 access-request lifecycle. const syntheticMissingAccessRequestFid = '9007199254740991'; const syntheticSecondAccessRequestFid = '9007199254740990'; -const historicalEntryAgreementVersions = Object.freeze([ - '2026-07-19-hegemony-entry-agreement-v3', - '2026-07-19-hegemony-entry-agreement-v2', - '2026-07-18-hegemony-entry-agreement-v1', - '2026-07-14', -]); -const alphaTermsVersion = '2026-07-31-hegemony-entry-agreement-v4'; +const historicalEntryAgreementVersions = + WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS; +const alphaTermsVersion = WARPKEEP_ENTRY_AGREEMENT_VERSION; const resourcePolicyVersion = 'genesis-resource-yield-v1'; const marksPolicyVersion = 'admitted-daily-mark-v1'; const profilePolicyVersion = 'trusted-snapchain-profile-v3'; diff --git a/spacetimedb/src/entryAgreementPolicy.ts b/spacetimedb/src/entryAgreementPolicy.ts index 66575af2..b98263bd 100644 --- a/spacetimedb/src/entryAgreementPolicy.ts +++ b/spacetimedb/src/entryAgreementPolicy.ts @@ -4,7 +4,11 @@ * "terms" for deployed wire compatibility only. */ export const WARPKEEP_ENTRY_AGREEMENT_VERSION = - '2026-07-31-hegemony-entry-agreement-v4'; + '2026-08-03-hegemony-entry-agreement-v5'; + +/** Production publication remains blocked until the coordinated rollout PR. */ +export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS = + 'review-only-rollout-blocked'; /** Compatibility alias retained by existing reducer and client imports. */ export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION; @@ -15,6 +19,7 @@ export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION; * entry or gameplay requirement, which always compares the exact current ID. */ export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS = Object.freeze([ + '2026-07-31-hegemony-entry-agreement-v4', '2026-07-19-hegemony-entry-agreement-v3', '2026-07-19-hegemony-entry-agreement-v2', '2026-07-18-hegemony-entry-agreement-v1', diff --git a/spacetimedb/tests/entryAgreementPolicy.test.ts b/spacetimedb/tests/entryAgreementPolicy.test.ts index 3cf4d217..7d953c3c 100644 --- a/spacetimedb/tests/entryAgreementPolicy.test.ts +++ b/spacetimedb/tests/entryAgreementPolicy.test.ts @@ -24,7 +24,7 @@ function source(path: string): string { test('the current Hegemony entry agreement preserves the deployed Terms-shaped version alias', () => { assert.equal( WARPKEEP_ENTRY_AGREEMENT_VERSION, - '2026-07-31-hegemony-entry-agreement-v4', + '2026-08-03-hegemony-entry-agreement-v5', ); assert.equal(WARPKEEP_ALPHA_TERMS_VERSION, WARPKEEP_ENTRY_AGREEMENT_VERSION); assert.equal(REEXPORTED_ALPHA_TERMS_VERSION, WARPKEEP_ENTRY_AGREEMENT_VERSION); @@ -33,6 +33,7 @@ test('the current Hegemony entry agreement preserves the deployed Terms-shaped v test('historical immutable evidence remains bounded and never becomes the current version', () => { assert.deepEqual(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS, [ + '2026-07-31-hegemony-entry-agreement-v4', '2026-07-19-hegemony-entry-agreement-v3', '2026-07-19-hegemony-entry-agreement-v2', '2026-07-18-hegemony-entry-agreement-v1', @@ -135,12 +136,12 @@ test('caller status reports missing exact-current evidence without mutation', () assert.ok(Object.isFrozen(result)); }); -test('a retained V3 acceptance is historical and cannot satisfy the V4 entry gate', () => { +test('a retained V4 acceptance is historical and cannot satisfy the V5 entry gate', () => { const fid = 101n; - const v3Version = '2026-07-19-hegemony-entry-agreement-v3'; - const v3Key = `${fid}:${v3Version}`; + const v4Version = '2026-07-31-hegemony-entry-agreement-v4'; + const v4Key = `${fid}:${v4Version}`; const retainedEvidence = new Map([ - [v3Key, Object.freeze({ acceptanceKey: v3Key, fid, termsVersion: v3Version })], + [v4Key, Object.freeze({ acceptanceKey: v4Key, fid, termsVersion: v4Version })], ]); const lookupKeys: string[] = []; diff --git a/src/legal/alphaTermsPolicy.ts b/src/legal/alphaTermsPolicy.ts index da0a1add..0f58df76 100644 --- a/src/legal/alphaTermsPolicy.ts +++ b/src/legal/alphaTermsPolicy.ts @@ -1,6 +1,6 @@ /** Exact version of the Social Contract incorporated by the current Terms. */ export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION = - '2026-07-19-HEGEMONY-SOCIAL-CONTRACT-V3'; + '2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4'; /** * Exact server-side identifier for the complete current entry-agreement bundle. @@ -8,7 +8,15 @@ export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION = * acceptance records bind the complete bundle rather than only one document. */ export const WARPKEEP_ENTRY_AGREEMENT_VERSION = - '2026-07-31-hegemony-entry-agreement-v4'; + '2026-08-03-hegemony-entry-agreement-v5'; + +/** + * Review-only bundles must never reach either production surface independently. + * The later coordinated rollout PR must change this exact value only after the + * owner/legal gate and browser/module compatibility proof are complete. + */ +export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS = + 'review-only-rollout-blocked'; /** * Retained deployed reducer/input name. It identifies the complete linked @@ -22,8 +30,15 @@ export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION; * allowing the accepted document to drift behind an unchanged reducer value. */ export const WARPKEEP_ALPHA_TERMS_TEXT_SHA256 = - 'b78bacc360df53f57bed668a68c311acf14e957156ecd8256e388a6ef38496bf'; + 'dede7757c3be767b7a87e89e2c68817e9390cde91fabcf38246756afacdf51bd'; /** SHA-256 of the canonical Social Contract's normalized visible

    text. */ export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256 = - 'a052a4f53aee749b702037f7a6eeb1e9dbd6fab0cbcd60aed81dacade8cbb66d'; + '85941d066dd39f5be069d640f1419491e6fc0f691d01c292bfc3ed995c249110'; + +/** Factual notice version published alongside the proposed entry agreement. */ +export const WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION = '2026-08-03-v6'; + +/** SHA-256 of the canonical Privacy Notice's normalized visible
    text. */ +export const WARPKEEP_ALPHA_PRIVACY_NOTICE_TEXT_SHA256 = + '79bd17b795b399391ed0f6f84f2c7ff35fdb3ae64bf4ca17e3df0ad67d7b361f'; diff --git a/src/legal/realmChatPolicy.ts b/src/legal/realmChatPolicy.ts new file mode 100644 index 00000000..2960fc08 --- /dev/null +++ b/src/legal/realmChatPolicy.ts @@ -0,0 +1,16 @@ +/** + * Review-only Realm Chat policy contract. + * + * This file does not grant client or server authority. The entry point must + * remain disabled until the owner/legal gate, server authority, moderation + * operations, release verification, and a separate activation record exist. + */ +export const WARPKEEP_REALM_CHAT_POLICY_VERSION = + '2026-08-03-realm-chat-policy-v1'; + +export const WARPKEEP_REALM_CHAT_CHANNEL_KEY = 'realm:genesis-001'; + +export const WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED = false; + +export const WARPKEEP_REALM_CHAT_REVIEW_STATUS = + 'pending-owner-and-qualified-legal-review'; diff --git a/tests/activationToolingSecurity.test.ts b/tests/activationToolingSecurity.test.ts index 48f4f1da..f324ba20 100644 --- a/tests/activationToolingSecurity.test.ts +++ b/tests/activationToolingSecurity.test.ts @@ -40,6 +40,7 @@ import { publishModule, readFoundedPublishExpectations, requireCanonicalPublishCoordinates, + requireEntryAgreementProductionRelease, runCurrentAdditiveMigrationProof, validateIssuerDeployment, verifyCanonicalDatabaseList, @@ -3197,6 +3198,19 @@ describe('activation publish safety', () => { })).not.toThrow(); }); + it('blocks production publication while the entry agreement remains review-only', () => { + expect(() => requireEntryAgreementProductionRelease()) + .toThrow(/entry agreement is review-only/i); + expect(() => requireEntryAgreementProductionRelease('production-approved')) + .not.toThrow(); + expect(() => requireEntryAgreementProductionRelease( + 'review-only-rollout-blocked', + true, + )).not.toThrow(); + expect(() => requireEntryAgreementProductionRelease('')) + .toThrow(/coordinated Pages and SpacetimeDB rollout approval/i); + }); + it('binds the repair operator to one recent private successful publication receipt', async () => { const root = await mkdtemp(join( realpathSync(tmpdir()), @@ -3362,13 +3376,13 @@ describe('activation publish safety', () => { WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '3', WARPKEEP_EXPECTED_FOUNDER_COUNT: '3', WARPKEEP_EXPECTED_PLAYER_COUNT: '1', - WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '6', + WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '7', })).toThrow(/expectations were invalid/i); expect(() => readFoundedPublishExpectations({ WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '100', WARPKEEP_EXPECTED_FOUNDER_COUNT: '100', WARPKEEP_EXPECTED_PLAYER_COUNT: '100', - WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '501', + WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '601', })).toThrow(/EXPECTED_TERMS_ACCEPTANCE_COUNT.*canonical integer/i); expect(() => readFoundedPublishExpectations({ WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '5', @@ -4976,7 +4990,7 @@ describe('protected aggregate child isolation', () => { }); const completeEntryAgreementHistoryAggregate = Object.freeze({ ...authenticatedGenesisV3FoundedAggregate, - alphaTermsAcceptances: '5', + alphaTermsAcceptances: '6', }); const genesisGenerationV3FoundedAggregate = Object.freeze({ ...genesisV3FoundedAggregate, @@ -5232,14 +5246,14 @@ describe('protected aggregate child isolation', () => { PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, 3, 1, - 5, + 6, )).not.toThrow(); expect(() => verifyExpectedAlphaV3Aggregate( JSON.stringify(authenticatedGenesisV3FoundedAggregate), PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, 3, 1, - 6, + 7, )).toThrow(/entry-agreement row count was invalid/i); }); @@ -5266,7 +5280,7 @@ describe('protected aggregate child isolation', () => { it.each([ ['player count', genesisV3FoundedAggregate, 4, 0], - ['entry-agreement row count', authenticatedGenesisV3FoundedAggregate, 1, 6], + ['entry-agreement row count', authenticatedGenesisV3FoundedAggregate, 1, 7], ])('rejects an expected %s above its bounded aggregate limit', (_label, aggregate, players, terms) => { expect(() => verifyExpectedAlphaV3Aggregate( JSON.stringify(aggregate), @@ -5866,7 +5880,7 @@ describe('protected aggregate child isolation', () => { ])).toThrow(/canonical integer/i); }); - it.each(['-1', '00', '01', '+1', '1.0', '1e2', '501', 'abc', '']) + it.each(['-1', '00', '01', '+1', '1.0', '1e2', '601', 'abc', '']) ('rejects noncanonical or globally out-of-range entry-agreement counts: %j', value => { expect(() => parseProductionVerifierArguments([ '--require-genesis-v3-founded-aggregate', @@ -5906,28 +5920,28 @@ describe('protected aggregate child isolation', () => { '--require-genesis-v3-founded-aggregate', '--expected-founder-count=3', '--expected-player-count=1', - '--expected-terms-acceptance-count=5', + '--expected-terms-acceptance-count=6', ])).toMatchObject({ expectedFounderCount: 3, expectedPlayerCount: 1, - expectedTermsAcceptanceCount: 5, + expectedTermsAcceptanceCount: 6, expectedEnabledAllowedFidCount: 3, }); expect(() => parseProductionVerifierArguments([ '--require-genesis-v3-founded-aggregate', '--expected-founder-count=3', '--expected-player-count=1', - '--expected-terms-acceptance-count=6', + '--expected-terms-acceptance-count=7', ])).toThrow(/supported immutable row history/i); expect(parseProductionVerifierArguments([ '--require-genesis-v3-founded-aggregate', '--expected-founder-count=100', '--expected-player-count=100', - '--expected-terms-acceptance-count=500', + '--expected-terms-acceptance-count=600', ])).toMatchObject({ expectedFounderCount: 100, expectedPlayerCount: 100, - expectedTermsAcceptanceCount: 500, + expectedTermsAcceptanceCount: 600, expectedEnabledAllowedFidCount: 100, }); }); diff --git a/tests/alphaTermsPolicy.test.ts b/tests/alphaTermsPolicy.test.ts index 22e92d06..f8cd1d33 100644 --- a/tests/alphaTermsPolicy.test.ts +++ b/tests/alphaTermsPolicy.test.ts @@ -5,8 +5,11 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { + WARPKEEP_ALPHA_PRIVACY_NOTICE_TEXT_SHA256, + WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION, WARPKEEP_ALPHA_TERMS_TEXT_SHA256, WARPKEEP_ALPHA_TERMS_VERSION, + WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS, WARPKEEP_ENTRY_AGREEMENT_VERSION, WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256, WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION, @@ -18,11 +21,13 @@ import { import { WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM, WARPKEEP_ENTRY_AGREEMENT_EVIDENCE_VERSIONS, + WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS as MODULE_ENTRY_AGREEMENT_RELEASE_STATUS, WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS, } from '../spacetimedb/src/entryAgreementPolicy'; import { WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM as TOOLING_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM, WARPKEEP_ENTRY_AGREEMENT_EVIDENCE_VERSIONS as TOOLING_ENTRY_AGREEMENT_EVIDENCE_VERSIONS, + WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS as TOOLING_ENTRY_AGREEMENT_RELEASE_STATUS, WARPKEEP_ENTRY_AGREEMENT_VERSION as TOOLING_ENTRY_AGREEMENT_VERSION, WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS as TOOLING_HISTORICAL_ENTRY_AGREEMENT_VERSIONS, } from '../scripts/entry-agreement-policy.mjs'; @@ -35,6 +40,10 @@ const socialContractHtml = readFileSync( resolve(dirname(fileURLToPath(import.meta.url)), '../public/social-contract/index.html'), 'utf8' ); +const privacyHtml = readFileSync( + resolve(dirname(fileURLToPath(import.meta.url)), '../public/privacy/index.html'), + 'utf8' +); function normalizedPublicDocumentText(html: string, documentName: string) { const parsedDocument = new DOMParser().parseFromString(html, 'text/html'); @@ -49,18 +58,27 @@ describe('versioned Alpha entry-agreement binding', () => { expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).toBe(MODULE_ENTRY_AGREEMENT_VERSION); expect(WARPKEEP_ALPHA_TERMS_VERSION).toBe(WARPKEEP_ENTRY_AGREEMENT_VERSION); expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).toBe( - '2026-07-31-hegemony-entry-agreement-v4', + '2026-08-03-hegemony-entry-agreement-v5', ); expect(WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION).toBe( - '2026-07-19-HEGEMONY-SOCIAL-CONTRACT-V3', + '2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4', ); expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).toBe( - '2026-07-31-hegemony-entry-agreement-v4', + '2026-08-03-hegemony-entry-agreement-v5', ); expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).not.toBe('2026-07-14'); + expect(WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS) + .toBe('review-only-rollout-blocked'); + expect(MODULE_ENTRY_AGREEMENT_RELEASE_STATUS) + .toBe(WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS); + expect(TOOLING_ENTRY_AGREEMENT_RELEASE_STATUS) + .toBe(WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS); + expect(WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION).toBe('2026-08-03-v6'); }); it('keeps historical evidence distinct from the current entry/gameplay version', () => { + expect(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS) + .toContain('2026-07-31-hegemony-entry-agreement-v4'); expect(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS) .toContain('2026-07-19-hegemony-entry-agreement-v3'); expect(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS).toContain('2026-07-14'); @@ -102,6 +120,16 @@ describe('versioned Alpha entry-agreement binding', () => { WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256, [WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION], ], + [ + 'Privacy Notice', + privacyHtml, + WARPKEEP_ALPHA_PRIVACY_NOTICE_TEXT_SHA256, + [ + WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION, + WARPKEEP_ENTRY_AGREEMENT_VERSION, + WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION, + ], + ], ] as const)( 'fails when canonical visible %s wording drifts without policy review', (documentName, html, expectedDigest, requiredVersions) => { diff --git a/tests/hermesAdminSecurity.test.ts b/tests/hermesAdminSecurity.test.ts index 4b4f8995..0c93c1aa 100644 --- a/tests/hermesAdminSecurity.test.ts +++ b/tests/hermesAdminSecurity.test.ts @@ -457,7 +457,7 @@ describe('Hermes machine-readable output', () => { const status = foundedGenerationV2Status(); expect(verifyGenesisExpansionPreconditionV3(status)).toEqual(status); - expect(WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM).toBe(5); + expect(WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM).toBe(6); const retainedHistoryStatus = { ...status, alphaTermsAcceptances: status.playersV2 diff --git a/tests/pagesDeployConfig.test.ts b/tests/pagesDeployConfig.test.ts index 4af4b6cb..5c768456 100644 --- a/tests/pagesDeployConfig.test.ts +++ b/tests/pagesDeployConfig.test.ts @@ -2,6 +2,9 @@ import { spawnSync } from 'node:child_process'; import { describe, expect, it } from 'vitest'; +// @ts-expect-error Repository JavaScript scripts intentionally expose test hooks. +import { validatePagesDeploymentConfiguration } from '../scripts/validate-pages-deploy-config.mjs'; + const FULL_SHA = 'abcdef0123456789abcdef0123456789abcdef01'; function deploymentEnvironment(overrides: Record = {}) { @@ -23,7 +26,7 @@ function deploymentEnvironment(overrides: Record = {}) { }; } -function validate(overrides?: Record) { +function validateCli(overrides?: Record) { return spawnSync(process.execPath, ['scripts/validate-pages-deploy-config.mjs'], { cwd: process.cwd(), encoding: 'utf8', @@ -31,7 +34,30 @@ function validate(overrides?: Record) { }); } +function validate(overrides?: Record) { + try { + const stdout = validatePagesDeploymentConfiguration( + deploymentEnvironment(overrides), + { entryAgreementReleaseStatus: 'production-approved' }, + ); + return { status: 0, stdout, stderr: '' }; + } catch (error) { + return { + status: 1, + stdout: '', + stderr: error instanceof Error ? error.message : String(error), + }; + } +} + describe('Pages deployment configuration validation', () => { + it('blocks the review-only agreement from the real deployment entry point', () => { + const result = validateCli(); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('entry agreement is review-only'); + expect(result.stderr).toContain('coordinated Pages and SpacetimeDB rollout approval'); + }); + it('accepts the root-base canonical build with shared alpha deliberately disabled', () => { const result = validate(); expect(result.status).toBe(0); diff --git a/tests/publicLegalDocuments.test.ts b/tests/publicLegalDocuments.test.ts index 306dc946..252a22ef 100644 --- a/tests/publicLegalDocuments.test.ts +++ b/tests/publicLegalDocuments.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { + WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION, WARPKEEP_ENTRY_AGREEMENT_VERSION, WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION, } from '../src/legal/alphaTermsPolicy'; @@ -19,6 +20,16 @@ const legalCss = readFileSync('public/legal/warpkeep-legal.css', 'utf8'); const strictPublicLegalCsp = "default-src 'none'; style-src 'self'; base-uri 'none'; form-action 'none'"; +const exactPublicLegalHrefAllowlist = new Set([ + '../', + './index.html', + '../terms/index.html', + '../social-contract/index.html', + '../privacy/index.html', + 'https://github.com/ael-dev3', + 'https://github.com/ael-dev3/Warpkeep', + 'https://github.com/ael-dev3/Warpkeep/security/policy', +]); function parse(html: string) { return new DOMParser().parseFromString(html, 'text/html'); @@ -110,6 +121,12 @@ describe('public Alpha legal documents', () => { expect(link.rel.split(/\s+/)).toEqual(expect.arrayContaining(['noopener', 'noreferrer'])); } + for (const link of document.querySelectorAll('a[href]')) { + expect(exactPublicLegalHrefAllowlist.has(link.getAttribute('href') ?? '')).toBe(true); + expect(link.hasAttribute('download')).toBe(false); + expect(link.hasAttribute('ping')).toBe(false); + } + for (const asset of document.querySelectorAll( 'img, source, video, audio, script, iframe, object, embed', )) { @@ -140,21 +157,37 @@ describe('public Alpha legal documents', () => { expect(termsText).toContain('Warpkeep is open source'); expect(termsText).toContain('does not guarantee that a suggestion'); expect(termsText).toContain('Access is allowlist gated and conditional'); + expect(termsText).toContain('If Realm Chat is later activated'); + expect(termsText).toContain('retained as Realm history'); + expect(termsText).toContain('warning is not required or guaranteed'); + expect(termsText).toContain('Good-faith criticism'); + expect(termsText).toContain('age and minor-participation policy is not final'); + expect(termsText).toContain('qualified legal reviewer'); expect(termsText).not.toContain('tokens, points, airdrops'); }); - it('keeps the Hegemony covenant concise about fiction, conduct, and unfinished systems', () => { + it('keeps the Hegemony covenant explicit about fiction, conduct, moderation, and disabled chat', () => { for (const expected of [ 'Hegemony', 'Article II', 'Article III', 'Article VI', + 'Article VII', 'Honest counsel strengthens', 'Criticizing Warpkeep', - 'Threats', + 'credible threats or incitement', + 'child sexual abuse material', + 'non-consensual intimate content', + 'unlawful hate or discriminatory abuse', + 'broad, good-faith, contextual judgment', + 'does not promise a formal appeal system', + 'false, retaliatory, or abusive reports', + 'Reporting must preserve the exact reported message', + 'local muting affects only', + 'disabled proposal pending owner and legal review', 'core strategy loop', 'Hegemony is game fiction', - 'warn, limit, suspend, or revoke', + 'A warning is not required or guaranteed', ]) expect(socialContractText).toContain(expected); for (const absent of ['Ouster', 'IP-level bans', 'Tribute is final']) { expect(socialContractText).not.toContain(absent); @@ -195,13 +228,24 @@ describe('public Alpha legal documents', () => { 'Hegemony Social Contract', WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION, WARPKEEP_ENTRY_AGREEMENT_VERSION, + WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION, 'cryptographically binds the exact visible Terms and Social Contract texts', + 'Realm Chat is not active in this release', + 'message body, channel, server-owned sequence and time', + 'permanent archive, report records, and moderation reasons', + 'current browser session', + 'without routine gameplay expiry', + 'restricted, tombstoned, anonymised, or erased', + 'Reporters do not receive another player\'s private moderation outcome', + 'age and minor-participation policy is unresolved', + 'No age threshold is asserted by this draft', ]) expect(privacyText).toContain(expected); expect(privacyText).toMatch(/FID.*entry.agreement.*accept(?:ed|ance).*(?:time|timestamp)/i); expect(privacyText).toMatch(/not (?:this )?Privacy Notice.*blanket privacy consent/i); expect(privacyText).not.toContain('world, player, faction'); expect(privacyText).not.toContain('tokens, points, airdrops'); + expect(privacyText).not.toContain('Realm Chat is active'); }); it('keeps the narrow-screen retention table a labelled keyboard scroll region', () => { diff --git a/tests/realmChatLegalPolicy.test.ts b/tests/realmChatLegalPolicy.test.ts new file mode 100644 index 00000000..c1ed52e1 --- /dev/null +++ b/tests/realmChatLegalPolicy.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { + WARPKEEP_REALM_CHAT_CHANNEL_KEY, + WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED, + WARPKEEP_REALM_CHAT_POLICY_VERSION, + WARPKEEP_REALM_CHAT_REVIEW_STATUS, +} from '../src/legal/realmChatPolicy'; + +describe('disabled Realm Chat legal contract', () => { + it('keeps the proposed social feature unavailable pending explicit review and activation', () => { + expect(WARPKEEP_REALM_CHAT_POLICY_VERSION) + .toBe('2026-08-03-realm-chat-policy-v1'); + expect(WARPKEEP_REALM_CHAT_CHANNEL_KEY).toBe('realm:genesis-001'); + expect(WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED).toBe(false); + expect(WARPKEEP_REALM_CHAT_REVIEW_STATUS) + .toBe('pending-owner-and-qualified-legal-review'); + }); +}); diff --git a/tests/warpkeepConnection.test.ts b/tests/warpkeepConnection.test.ts index 55da907c..82dc422e 100644 --- a/tests/warpkeepConnection.test.ts +++ b/tests/warpkeepConnection.test.ts @@ -1215,6 +1215,10 @@ describe('Warpkeep authenticated connection boundary', () => { { requiredVersion: '2026-07-19-hegemony-entry-agreement-v2', acceptedCurrent: true + }, + { + requiredVersion: '2026-07-31-hegemony-entry-agreement-v4', + acceptedCurrent: true } ])('rejects malformed or mismatched entry-agreement status %#', async raw => { const connection = { @@ -1658,7 +1662,7 @@ describe('Warpkeep authenticated connection boundary', () => { it('pins the browser and authoritative module to the same Terms version', () => { expect(BROWSER_ALPHA_TERMS_VERSION).toBe(MODULE_ALPHA_TERMS_VERSION); expect(BROWSER_ALPHA_TERMS_VERSION).toBe( - '2026-07-31-hegemony-entry-agreement-v4' + '2026-08-03-hegemony-entry-agreement-v5' ); }); diff --git a/tests/workflowSecurity.test.ts b/tests/workflowSecurity.test.ts index a5c41b74..dd0dcd2d 100644 --- a/tests/workflowSecurity.test.ts +++ b/tests/workflowSecurity.test.ts @@ -89,6 +89,10 @@ describe('GitHub workflow security policy', () => { expect(build).toContain( 'WARPKEEP_SHARED_ALPHA_ENABLED must be exactly true or false.', ); + expect(build).toContain('npm run validate:pages-config'); + expect(build.indexOf('npm run validate:pages-config')).toBeLessThan( + build.indexOf('npm run build'), + ); expect(source).toContain('group: pages-main'); expect(source).not.toMatch(/^\s+group:\s*pages\s*$/m); });