diff --git a/docs/operations/alpha-activation.md b/docs/operations/alpha-activation.md index 0e3feb2..82d2b7b 100644 --- a/docs/operations/alpha-activation.md +++ b/docs/operations/alpha-activation.md @@ -105,6 +105,18 @@ artifact, pinned CLI, issuer, expectation format, and selected stage contract; it does not inspect Maincloud or publish. Review the result, then use the same explicit stage arguments without `--dry-run` and with the publisher's exact confirmation variable set through the private operator environment. + +The private operator environment must also supply all four founded-state +counts: `WARPKEEP_EXPECTED_FOUNDER_COUNT`, +`WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT`, +`WARPKEEP_EXPECTED_PLAYER_COUNT`, and +`WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT`. Founder rows remain the durable +castle-state count; the enabled allowlist count may be lower after an admission +is revoked. Both are checked independently before and after publication. +The standalone production verifier accepts the corresponding +`--expected-enabled-allowed-fid-count=` founded-stage flag; omitting it +keeps the compatibility assertion that every founder remains enabled. + Do not substitute raw `spacetime publish` commands. If publication times out or returns an ambiguous result, do not republish. A fresh read-only inspection must establish the live schema and counts before any further release decision. diff --git a/scripts/publish-spacetime-dev.d.mts b/scripts/publish-spacetime-dev.d.mts index 4c216d7..1afedc2 100644 --- a/scripts/publish-spacetime-dev.d.mts +++ b/scripts/publish-spacetime-dev.d.mts @@ -16,6 +16,13 @@ export const ACCESS_REQUEST_V13_TABLE_CONTRACTS: Readonly>>; +export interface FoundedPublishExpectations { + readonly expectedEnabledAllowedFidCount: number; + readonly expectedFounderCount: number; + readonly expectedPlayerCount: number; + readonly expectedTermsAcceptanceCount: number; +} + export function alphaV8AggregateChildArguments(...args: any[]): any; export function alphaV10AggregateChildArguments(...args: any[]): any; export function alphaV12AggregateChildArguments(...args: any[]): any; @@ -36,7 +43,9 @@ export function publishPostV12AggregateChildArguments(...args: any[]): any; export function publishPreV12AggregateChildArguments(...args: any[]): any; export function publishChildEnvironment(...args: any[]): any; export function publishModule(...args: any[]): any; -export function readFoundedPublishExpectations(...args: any[]): any; +export function readFoundedPublishExpectations( + source?: Record, +): Readonly; export function requireCanonicalPublishCoordinates(...args: any[]): any; export function runCurrentAdditiveMigrationProof(...args: any[]): any; export function validateIssuerDeployment(...args: any[]): any; @@ -44,12 +53,24 @@ export function verifyCanonicalDatabaseList(...args: any[]): any; export function verifyFreshAlphaStatusV8Aggregate(...args: any[]): any; export function verifyFreshAlphaStatusV10Aggregate(...args: any[]): any; export function verifyFreshAlphaStatusV12Aggregate(...args: any[]): any; -export function verifyFreshPublishExactV12Aggregate(...args: any[]): any; -export function verifyFreshPublishPreV12Aggregate(...args: any[]): any; +export function verifyFreshPublishExactV12Aggregate( + secret: string, + expectations: FoundedPublishExpectations, + ...args: any[] +): any; +export function verifyFreshPublishPreV12Aggregate( + secret: string, + expectations: FoundedPublishExpectations, + ...args: any[] +): any; export function verifyFreshProductionV11Schema(...args: any[]): any; export function verifyFreshProductionV12ModuleSchema(...args: any[]): any; export function verifyFreshProductionV13ModuleSchema(...args: any[]): any; -export function verifyFreshFoundedProtocolV3Aggregate(...args: any[]): any; +export function verifyFreshFoundedProtocolV3Aggregate( + secret: string, + expectations: FoundedPublishExpectations, + ...args: any[] +): any; export function verifyFreshResourceProtocolV4PrebackfillAggregate(...args: any[]): any; export function verifyFreshResourceProtocolV4ReadyAggregate(...args: any[]): any; export function verifyMigrationArtifactReceipt(...args: any[]): any; @@ -57,8 +78,16 @@ export function verifyPinnedCliAttestation(...args: any[]): any; export function verifyPostPublishAlphaStatusV8Aggregate(...args: any[]): any; export function verifyPostPublishAlphaStatusV10Aggregate(...args: any[]): any; export function verifyPostPublishAlphaStatusV12Aggregate(...args: any[]): any; -export function verifyPostPublishCombinedV12Aggregate(...args: any[]): any; -export function verifyPostPublishFoundedProtocolV3Aggregate(...args: any[]): any; +export function verifyPostPublishCombinedV12Aggregate( + secret: string, + expectations: FoundedPublishExpectations, + ...args: any[] +): any; +export function verifyPostPublishFoundedProtocolV3Aggregate( + secret: string, + expectations: FoundedPublishExpectations, + ...args: any[] +): any; export function verifyPostPublishProductionV12Schema(...args: any[]): any; export function verifyPostPublishProductionV12ModuleSchema(...args: any[]): any; export function verifyPostPublishProductionV13ModuleSchema(...args: any[]): any; @@ -66,7 +95,11 @@ export function verifyPostPublishProductionV13ActiveModuleSchema(...args: any[]) export function verifyPostPublishProductionV13SchemaFromV11(...args: any[]): any; export function verifyPostPublishResourceProtocolV4PrebackfillAggregate(...args: any[]): any; export function verifyPostPublishResourceProtocolV4ReadyAggregate(...args: any[]): any; -export function verifyPostPublishResourcePublicationCheckpoints(...args: any[]): any; +export function verifyPostPublishResourcePublicationCheckpoints( + secret: string, + expectations: FoundedPublishExpectations, + ...args: any[] +): any; export function verifyPrivacySafeAlphaStatusV8Output(...args: any[]): any; export function verifyPrivacySafeAlphaStatusV10Output(...args: any[]): any; export function verifyPrivacySafeAlphaStatusV12Output(...args: any[]): any; diff --git a/scripts/publish-spacetime-dev.mjs b/scripts/publish-spacetime-dev.mjs index 17f8f53..9a76d74 100644 --- a/scripts/publish-spacetime-dev.mjs +++ b/scripts/publish-spacetime-dev.mjs @@ -1135,6 +1135,7 @@ function validateFoundedPublishExpectations(value) { value === null || typeof value !== 'object' || Object.keys(value).sort().join(',') !== [ + 'expectedEnabledAllowedFidCount', 'expectedFounderCount', 'expectedPlayerCount', 'expectedTermsAcceptanceCount', @@ -1143,6 +1144,7 @@ function validateFoundedPublishExpectations(value) { fail('Exact founded protocol-v3 publication expectations are required.'); } const { + expectedEnabledAllowedFidCount, expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, @@ -1151,6 +1153,9 @@ function validateFoundedPublishExpectations(value) { !Number.isSafeInteger(expectedFounderCount) || expectedFounderCount < 1 || expectedFounderCount > 100 + || !Number.isSafeInteger(expectedEnabledAllowedFidCount) + || expectedEnabledAllowedFidCount < 0 + || expectedEnabledAllowedFidCount > expectedFounderCount || !Number.isSafeInteger(expectedPlayerCount) || expectedPlayerCount < 0 || expectedPlayerCount > expectedFounderCount @@ -1162,6 +1167,7 @@ function validateFoundedPublishExpectations(value) { fail('Founded protocol-v3 publication expectations were invalid.'); } return Object.freeze({ + expectedEnabledAllowedFidCount, expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, @@ -1183,6 +1189,10 @@ export function readFoundedPublishExpectations(source = process.env) { }; return validateFoundedPublishExpectations({ expectedFounderCount: readCount('WARPKEEP_EXPECTED_FOUNDER_COUNT', 1), + expectedEnabledAllowedFidCount: readCount( + 'WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT', + 0, + ), expectedPlayerCount: readCount('WARPKEEP_EXPECTED_PLAYER_COUNT', 0), expectedTermsAcceptanceCount: readCount( 'WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT', @@ -2375,6 +2385,7 @@ export function verifyFreshFoundedProtocolV3Aggregate( exactExpectations.expectedFounderCount, exactExpectations.expectedPlayerCount, exactExpectations.expectedTermsAcceptanceCount, + exactExpectations.expectedEnabledAllowedFidCount, ); } @@ -2962,6 +2973,7 @@ function verifyCombinedProtocolV3AndResourceV4( expectations.expectedFounderCount, expectations.expectedPlayerCount, expectations.expectedTermsAcceptanceCount, + expectations.expectedEnabledAllowedFidCount, ); if (resourceRolloutStage === RESOURCE_PUBLISH_ROLLOUT_STAGE.PREBACKFILL) { verifyExpectedAlphaV4ResourcePrebackfillAggregate( diff --git a/scripts/verify-alpha-production.mjs b/scripts/verify-alpha-production.mjs index dbe19e5..7a5bb5c 100644 --- a/scripts/verify-alpha-production.mjs +++ b/scripts/verify-alpha-production.mjs @@ -1449,11 +1449,16 @@ function expectedV3StateCounts( expectedFounderCount, expectedPlayerCount = 0, expectedTermsAcceptanceCount = 0, + expectedEnabledAllowedFidCount = expectedFounderCount, ) { if ( stage !== PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED && stage !== PROTECTED_AGGREGATE_STAGE.GENESIS_GENERATION_V3_FOUNDED - && (expectedPlayerCount !== 0 || expectedTermsAcceptanceCount !== 0) + && ( + expectedPlayerCount !== 0 + || expectedTermsAcceptanceCount !== 0 + || expectedEnabledAllowedFidCount !== undefined + ) ) { fail('protocol-v3 authenticated count expectations require the founded aggregate stage.'); } @@ -1477,6 +1482,11 @@ function expectedV3StateCounts( expectedTermsAcceptanceCount, players, ); + const enabledAllowedFids = readExpectedFoundedMutableCount( + expectedEnabledAllowedFidCount, + 'enabled allowed-FID count', + founders, + ); return Object.freeze({ ...(stage === PROTECTED_AGGREGATE_STAGE.GENESIS_GENERATION_V3_FOUNDED ? EXPECTED_GENESIS_GENERATION_V3_COUNTS @@ -1490,7 +1500,7 @@ function expectedV3StateCounts( markAccounts: founders, alphaTermsAcceptances: termsAcceptances, allowedFids: founders, - enabledAllowedFids: founders, + enabledAllowedFids, }); } fail('protocol-v3 aggregate verification stage was invalid.'); @@ -1502,12 +1512,14 @@ export function verifyExpectedAlphaV3Aggregate( expectedFounderCount, expectedPlayerCount = 0, expectedTermsAcceptanceCount = 0, + expectedEnabledAllowedFidCount = expectedFounderCount, ) { const expectedCounts = expectedV3StateCounts( stage, expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, + expectedEnabledAllowedFidCount, ); let status; try { @@ -1774,6 +1786,7 @@ function verifyProtectedAggregateIfConfigured( expectedFounderCount, expectedPlayerCount = 0, expectedTermsAcceptanceCount = 0, + expectedEnabledAllowedFidCount = expectedFounderCount, ) { const normalizedStage = normalizeProtectedAggregateStage(stage); const secret = requiredProtectedAggregateSecret(process.env.WARPKEEP_ADMIN_TOKEN_SECRET, required); @@ -1802,6 +1815,7 @@ function verifyProtectedAggregateIfConfigured( expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, + expectedEnabledAllowedFidCount, ); } // Never mirror child-process output: even a future Hermes implementation @@ -1827,6 +1841,7 @@ export function verifyPostBackfillResourceAggregateCheckpoints( repositoryRoot = resolve(dirname(resolve(process.argv[1])), '..'), sourceEnvironment = process.env, foundedStage = PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + expectedEnabledAllowedFidCount = expectedFounderCount, ) { const requiredSecret = requiredProtectedAggregateSecret(secret, true); const tsxCli = resolve(repositoryRoot, 'node_modules/tsx/dist/cli.mjs'); @@ -1856,6 +1871,7 @@ export function verifyPostBackfillResourceAggregateCheckpoints( expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, + expectedEnabledAllowedFidCount, ); console.log( foundedStage === PROTECTED_AGGREGATE_STAGE.GENESIS_GENERATION_V3_FOUNDED @@ -1893,6 +1909,7 @@ export function parseProductionVerifierArguments(arguments_ = process.argv.slice let expectedFounderCount; let expectedPlayerCount; let expectedTermsAcceptanceCount; + let expectedEnabledAllowedFidCount; for (const argument of arguments_) { if (argument.startsWith('--expected-founder-count=')) { if (expectedFounderCount !== undefined) { @@ -1933,6 +1950,17 @@ export function parseProductionVerifierArguments(arguments_ = process.argv.slice expectedTermsAcceptanceCount = Number(value); continue; } + if (argument.startsWith('--expected-enabled-allowed-fid-count=')) { + if (expectedEnabledAllowedFidCount !== undefined) { + fail('unknown or duplicate command-line argument.'); + } + const value = argument.slice('--expected-enabled-allowed-fid-count='.length); + if (!/^(?:0|[1-9]|[1-9]\d|100)$/.test(value)) { + fail('expected enabled allowed-FID count must be a canonical integer from 0 through 100.'); + } + expectedEnabledAllowedFidCount = Number(value); + continue; + } if (!allowed.has(argument) || seen.has(argument)) { fail('unknown or duplicate command-line argument.'); } @@ -1964,7 +1992,11 @@ export function parseProductionVerifierArguments(arguments_ = process.argv.slice } if ( !requiresFoundedAggregate - && (expectedPlayerCount !== undefined || expectedTermsAcceptanceCount !== undefined) + && ( + expectedPlayerCount !== undefined + || expectedTermsAcceptanceCount !== undefined + || expectedEnabledAllowedFidCount !== undefined + ) ) { fail('authenticated count expectations require the founded aggregate stage.'); } @@ -1976,10 +2008,14 @@ export function parseProductionVerifierArguments(arguments_ = process.argv.slice } const foundedPlayerCount = expectedPlayerCount ?? 0; const foundedTermsAcceptanceCount = expectedTermsAcceptanceCount ?? 0; + const foundedEnabledAllowedFidCount = requiresFoundedAggregate + ? (expectedEnabledAllowedFidCount ?? expectedFounderCount) + : undefined; if ( requiresFoundedAggregate && ( foundedPlayerCount > expectedFounderCount + || foundedEnabledAllowedFidCount > expectedFounderCount ) ) { fail('authenticated count expectations cannot exceed the expected founder count.'); @@ -2003,6 +2039,7 @@ export function parseProductionVerifierArguments(arguments_ = process.argv.slice expectedFounderCount, expectedPlayerCount: foundedPlayerCount, expectedTermsAcceptanceCount: foundedTermsAcceptanceCount, + expectedEnabledAllowedFidCount: foundedEnabledAllowedFidCount, requireAuthV2: seen.has('--require-auth-v2'), requireAuthV2Enabled: seen.has('--require-auth-v2-enabled'), aggregateStage, @@ -2021,6 +2058,7 @@ async function main() { expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, + expectedEnabledAllowedFidCount, requireAuthV2, requireAuthV2Enabled, aggregateStage, @@ -2055,6 +2093,7 @@ async function main() { resolve(dirname(resolve(process.argv[1])), '..'), process.env, aggregateStage, + expectedEnabledAllowedFidCount, ); } else { verifyProtectedAggregateIfConfigured( @@ -2069,6 +2108,7 @@ async function main() { expectedFounderCount, expectedPlayerCount, expectedTermsAcceptanceCount, + expectedEnabledAllowedFidCount, ); } } diff --git a/tests/activationToolingSecurity.test.ts b/tests/activationToolingSecurity.test.ts index 0e6add1..cc6df0c 100644 --- a/tests/activationToolingSecurity.test.ts +++ b/tests/activationToolingSecurity.test.ts @@ -2667,86 +2667,120 @@ describe('activation publish safety', () => { it('requires exact canonical founded-state expectations for a live republish', () => { const expectations = readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '3', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_PLAYER_COUNT: '1', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '1', }); expect(expectations).toEqual({ + expectedEnabledAllowedFidCount: 3, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, }); expect(Object.isFrozen(expectations)).toBe(true); expect(readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '0', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_PLAYER_COUNT: '0', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '0', })).toEqual({ + expectedEnabledAllowedFidCount: 0, expectedFounderCount: 4, expectedPlayerCount: 0, expectedTermsAcceptanceCount: 0, }); expect(readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '4', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_PLAYER_COUNT: '1', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '2', })).toEqual({ + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 2, }); expect(readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '99', WARPKEEP_EXPECTED_FOUNDER_COUNT: '100', WARPKEEP_EXPECTED_PLAYER_COUNT: '100', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '300', })).toEqual({ + expectedEnabledAllowedFidCount: 99, expectedFounderCount: 100, expectedPlayerCount: 100, expectedTermsAcceptanceCount: 300, }); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '0', WARPKEEP_EXPECTED_PLAYER_COUNT: '0', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '0', })).toThrow(/EXPECTED_FOUNDER_COUNT.*canonical integer/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', + WARPKEEP_EXPECTED_PLAYER_COUNT: '0', + WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '0', + })).toThrow(/EXPECTED_ENABLED_ALLOWED_FID_COUNT.*canonical integer/i); + expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '4', WARPKEEP_EXPECTED_FOUNDER_COUNT: '04', WARPKEEP_EXPECTED_PLAYER_COUNT: '1', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '1', })).toThrow(/canonical integer/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '4', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '0', })).toThrow(/EXPECTED_PLAYER_COUNT.*canonical integer/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '4', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_PLAYER_COUNT: '0', })).toThrow(/EXPECTED_TERMS_ACCEPTANCE_COUNT.*canonical integer/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '4', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_PLAYER_COUNT: '01', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '0', })).toThrow(/EXPECTED_PLAYER_COUNT.*canonical integer/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '4', WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', WARPKEEP_EXPECTED_PLAYER_COUNT: '1', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '01', })).toThrow(/EXPECTED_TERMS_ACCEPTANCE_COUNT.*canonical integer/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '3', WARPKEEP_EXPECTED_FOUNDER_COUNT: '3', WARPKEEP_EXPECTED_PLAYER_COUNT: '4', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '1', })).toThrow(/expectations were invalid/i); expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '3', WARPKEEP_EXPECTED_FOUNDER_COUNT: '3', WARPKEEP_EXPECTED_PLAYER_COUNT: '1', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '5', })).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: '401', })).toThrow(/EXPECTED_TERMS_ACCEPTANCE_COUNT.*canonical integer/i); + expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '5', + WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', + WARPKEEP_EXPECTED_PLAYER_COUNT: '1', + WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '1', + })).toThrow(/expectations were invalid/i); + expect(() => readFoundedPublishExpectations({ + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '04', + WARPKEEP_EXPECTED_FOUNDER_COUNT: '4', + WARPKEEP_EXPECTED_PLAYER_COUNT: '1', + WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '1', + })).toThrow(/EXPECTED_ENABLED_ALLOWED_FID_COUNT.*canonical integer/i); }); it('pins the exact CLI build and canonical existing database identity', () => { @@ -2832,6 +2866,16 @@ describe('activation publish safety', () => { expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, + } as never, + fakeSpawnSync, + )).toThrow(/expectations are required/i); + expect(() => verifyFreshFoundedProtocolV3Aggregate( + testSecret, + { + expectedEnabledAllowedFidCount: 4, + expectedFounderCount: 4, + expectedPlayerCount: 1, + expectedTermsAcceptanceCount: 1, }, fakeSpawnSync, )).not.toThrow(); @@ -2859,6 +2903,7 @@ describe('activation publish safety', () => { expect(() => verifyFreshFoundedProtocolV3Aggregate( testSecret, { + expectedEnabledAllowedFidCount: 5, expectedFounderCount: 5, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -2868,16 +2913,18 @@ describe('activation publish safety', () => { expect(() => verifyFreshFoundedProtocolV3Aggregate( testSecret, { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, extra: true, - }, + } as never, fakeSpawnSync, )).toThrow(/expectations are required/i); expect(() => verifyFreshFoundedProtocolV3Aggregate( testSecret, { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -2889,6 +2936,7 @@ describe('activation publish safety', () => { const postPublishFailure = () => verifyPostPublishFoundedProtocolV3Aggregate( 'TEST_ONLY_HERMES_SECRET_'.repeat(2), { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -3020,6 +3068,7 @@ describe('activation publish safety', () => { expect(() => verifyPostPublishResourcePublicationCheckpoints( testSecret, { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -3038,6 +3087,7 @@ describe('activation publish safety', () => { expect(() => verifyPostPublishResourcePublicationCheckpoints( testSecret, { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -3049,6 +3099,7 @@ describe('activation publish safety', () => { expect(() => verifyPostPublishResourcePublicationCheckpoints( testSecret, { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -3062,12 +3113,16 @@ describe('activation publish safety', () => { it('uses exactly one bounded token-bearing child before and after publish for every aggregate checkpoint', () => { const secret = 'TEST_ONLY_HERMES_SECRET_'.repeat(2); const expectations = { + expectedEnabledAllowedFidCount: 3, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, }; const preEnvelope = { - protocolV3: publishProtocolV3Status(), + protocolV3: { + ...publishProtocolV3Status(), + enabledAllowedFids: '3', + }, resourceV4: publishResourceV4Status(), }; const postEnvelope = { @@ -3148,6 +3203,7 @@ describe('activation publish safety', () => { it('requires the exact selected v12 aggregate before a code-only republish', () => { const secret = 'TEST_ONLY_HERMES_SECRET_'.repeat(2); const expectations = { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -3357,6 +3413,7 @@ describe('activation publish safety', () => { expect(() => verifyPostPublishCombinedV12Aggregate( 'TEST_ONLY_HERMES_SECRET_'.repeat(2), { + expectedEnabledAllowedFidCount: 4, expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, @@ -4321,6 +4378,10 @@ describe('protected aggregate child isolation', () => { worldTiles: '10000', worldTileMeta: '10000', }); + const foundedAggregateWithRevokedAdmission = Object.freeze({ + ...genesisV3FoundedAggregate, + enabledAllowedFids: '2', + }); it('accepts only exact legacy and additive-v2 aggregate objects', () => { expect(() => verifyExpectedAlphaAggregate(JSON.stringify({ @@ -4347,6 +4408,14 @@ describe('protected aggregate child isolation', () => { PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, 3, )).not.toThrow(); + expect(() => verifyExpectedAlphaV3Aggregate( + JSON.stringify(foundedAggregateWithRevokedAdmission), + PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + 3, + 0, + 0, + 2, + )).not.toThrow(); expect(() => verifyExpectedAlphaV3Aggregate( JSON.stringify(authenticatedGenesisV3FoundedAggregate), PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, @@ -4580,6 +4649,14 @@ describe('protected aggregate child isolation', () => { 1, 1, )).toThrow(/require the founded aggregate stage/i); + expect(() => verifyExpectedAlphaV3Aggregate( + JSON.stringify(fixture), + stage, + undefined, + 0, + 0, + 0, + )).toThrow(/require the founded aggregate stage/i); }); it.each([ @@ -4602,7 +4679,6 @@ describe('protected aggregate child isolation', () => { 'realmProfiles', 'markAccounts', 'allowedFids', - 'enabledAllowedFids', ])('requires founded-stage %s to equal the private expected count', field => { expect(() => verifyExpectedAlphaV3Aggregate( JSON.stringify({ ...genesisV3FoundedAggregate, [field]: '2' }), @@ -4611,6 +4687,38 @@ describe('protected aggregate child isolation', () => { )).toThrow(/rollout stage/i); }); + it('keeps founder state while checking the independently enabled admission count', () => { + expect(() => verifyExpectedAlphaV3Aggregate( + JSON.stringify(foundedAggregateWithRevokedAdmission), + PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + 3, + )).toThrow(/rollout stage/i); + expect(() => verifyExpectedAlphaV3Aggregate( + JSON.stringify(genesisV3FoundedAggregate), + PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + 3, + 0, + 0, + 2, + )).toThrow(/rollout stage/i); + expect(() => verifyExpectedAlphaV3Aggregate( + JSON.stringify(genesisV3FoundedAggregate), + PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + 3, + 0, + 0, + 4, + )).toThrow(/enabled allowed-FID count was invalid/i); + expect(() => verifyExpectedAlphaV3Aggregate( + JSON.stringify(genesisV3FoundedAggregate), + PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + 3, + 0, + 0, + -1, + )).toThrow(/enabled allowed-FID count was invalid/i); + }); + it.each([ ['missing field', (() => { const value = { ...additiveV3PreseedAggregate } as Record; @@ -4759,6 +4867,10 @@ describe('protected aggregate child isolation', () => { protocolVersion: 3, resourcePolicyVersion: 'genesis-resource-yield-v1', }; + const foundedAggregate = { + ...authenticatedGenesisV3FoundedAggregate, + enabledAllowedFids: '2', + }; const fakeSpawnSync = (...args: unknown[]) => { calls.push(args); const childArguments = args[1] as string[]; @@ -4767,7 +4879,7 @@ describe('protected aggregate child isolation', () => { signal: null, stdout: JSON.stringify( childArguments.includes('inspect-alpha-v3') - ? authenticatedGenesisV3FoundedAggregate + ? foundedAggregate : aggregate, ), stderr: '', @@ -4784,6 +4896,8 @@ describe('protected aggregate child isolation', () => { fakeSpawnSync, repositoryRoot, exactEnvironment, + PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, + 2, )).not.toThrow(); expect(calls).toHaveLength(2); expect(calls[0]?.[0]).toBe(process.execPath); @@ -4826,7 +4940,7 @@ describe('protected aggregate child isolation', () => { 'alpha status: required resource procedure-v4 ready aggregate state verified', ); expect(JSON.stringify(log.mock.calls)).not.toContain( - JSON.stringify(authenticatedGenesisV3FoundedAggregate), + JSON.stringify(foundedAggregate), ); expect(JSON.stringify(log.mock.calls)).not.toContain(JSON.stringify(aggregate)); } finally { @@ -4910,6 +5024,7 @@ describe('protected aggregate child isolation', () => { expectedFounderCount: undefined, expectedPlayerCount: 0, expectedTermsAcceptanceCount: 0, + expectedEnabledAllowedFidCount: undefined, requireAuthV2: false, requireAuthV2Enabled: false, aggregateStage: PROTECTED_AGGREGATE_STAGE.LEGACY, @@ -4961,6 +5076,7 @@ describe('protected aggregate child isolation', () => { ...defaults, requireGenesisV3FoundedAggregate: true, expectedFounderCount: 3, + expectedEnabledAllowedFidCount: 3, aggregateStage: PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, }); expect(parseProductionVerifierArguments([ @@ -4971,6 +5087,7 @@ describe('protected aggregate child isolation', () => { requireGenesisV3FoundedAggregate: true, requireGenesisGenerationV3FoundedAggregate: true, expectedFounderCount: 3, + expectedEnabledAllowedFidCount: 3, aggregateStage: PROTECTED_AGGREGATE_STAGE.GENESIS_GENERATION_V3_FOUNDED, }); expect(parseProductionVerifierArguments([ @@ -4987,6 +5104,7 @@ describe('protected aggregate child isolation', () => { expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, + expectedEnabledAllowedFidCount: 4, requireAuthV2Enabled: true, aggregateStage: PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, }); @@ -5001,6 +5119,7 @@ describe('protected aggregate child isolation', () => { expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 1, + expectedEnabledAllowedFidCount: 4, aggregateStage: PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, }); expect(parseProductionVerifierArguments([ @@ -5014,8 +5133,18 @@ describe('protected aggregate child isolation', () => { expectedFounderCount: 4, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 2, + expectedEnabledAllowedFidCount: 4, aggregateStage: PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED, }); + expect(parseProductionVerifierArguments([ + '--require-genesis-generation-v3-founded-aggregate', + '--expected-founder-count=8', + '--expected-enabled-allowed-fid-count=7', + ])).toMatchObject({ + expectedFounderCount: 8, + expectedEnabledAllowedFidCount: 7, + aggregateStage: PROTECTED_AGGREGATE_STAGE.GENESIS_GENERATION_V3_FOUNDED, + }); expect(() => parseProductionVerifierArguments(['--require-auth-v3'])) .toThrow(/unknown or duplicate/i); expect(() => parseProductionVerifierArguments(['--require-genesis-v2-seeded-empty-aggregate'])) @@ -5080,6 +5209,7 @@ describe('protected aggregate child isolation', () => { it.each([ [['--expected-player-count=0']], [['--expected-terms-acceptance-count=0']], + [['--expected-enabled-allowed-fid-count=0']], [['--require-additive-v3-preseed-aggregate', '--expected-player-count=0']], ])('rejects founded authenticated expectations at another stage: %j', arguments_ => { expect(() => parseProductionVerifierArguments(arguments_)) @@ -5113,6 +5243,15 @@ describe('protected aggregate child isolation', () => { ])).toThrow(/canonical integer/i); }); + it.each(['-1', '00', '01', '+1', '1.0', '1e2', '101', 'abc', '']) + ('rejects noncanonical or out-of-range enabled allowed-FID counts: %j', value => { + expect(() => parseProductionVerifierArguments([ + '--require-genesis-v3-founded-aggregate', + '--expected-founder-count=3', + `--expected-enabled-allowed-fid-count=${value}`, + ])).toThrow(/canonical integer/i); + }); + it.each(['-1', '00', '01', '+1', '1.0', '1e2', '401', 'abc', '']) ('rejects noncanonical or globally out-of-range entry-agreement counts: %j', value => { expect(() => parseProductionVerifierArguments([ @@ -5125,6 +5264,7 @@ describe('protected aggregate child isolation', () => { it.each([ '--expected-player-count=1', '--expected-terms-acceptance-count=1', + '--expected-enabled-allowed-fid-count=1', ])('rejects duplicate authenticated count argument %s', argument => { expect(() => parseProductionVerifierArguments([ '--require-genesis-v3-founded-aggregate', @@ -5140,6 +5280,11 @@ describe('protected aggregate child isolation', () => { '--expected-founder-count=3', '--expected-player-count=4', ])).toThrow(/cannot exceed/i); + expect(() => parseProductionVerifierArguments([ + '--require-genesis-v3-founded-aggregate', + '--expected-founder-count=3', + '--expected-enabled-allowed-fid-count=4', + ])).toThrow(/cannot exceed/i); }); it('allows the complete immutable acceptance history per player but fails closed above it', () => { @@ -5152,6 +5297,7 @@ describe('protected aggregate child isolation', () => { expectedFounderCount: 3, expectedPlayerCount: 1, expectedTermsAcceptanceCount: 4, + expectedEnabledAllowedFidCount: 3, }); expect(() => parseProductionVerifierArguments([ '--require-genesis-v3-founded-aggregate', @@ -5168,6 +5314,7 @@ describe('protected aggregate child isolation', () => { expectedFounderCount: 100, expectedPlayerCount: 100, expectedTermsAcceptanceCount: 300, + expectedEnabledAllowedFidCount: 100, }); }); @@ -5183,6 +5330,7 @@ describe('protected aggregate child isolation', () => { HOME: '/test/home', WARPKEEP_UNRELATED_SECRET_SENTINEL: 'must-not-be-forwarded', WARPKEEP_ADMIN_TOKEN_SECRET: 'must-not-be-forwarded', + WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: 'must-not-be-forwarded', WARPKEEP_EXPECTED_FOUNDER_COUNT: 'must-not-be-forwarded', WARPKEEP_EXPECTED_PLAYER_COUNT: 'must-not-be-forwarded', WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: 'must-not-be-forwarded',