diff --git a/docs/officers/LOW_COST_COMMUNITY.md b/docs/officers/LOW_COST_COMMUNITY.md index 45d442d..f055385 100644 --- a/docs/officers/LOW_COST_COMMUNITY.md +++ b/docs/officers/LOW_COST_COMMUNITY.md @@ -97,6 +97,8 @@ The #683 source change configured no budget, spending cap, artifact-cleanup poli ## Engineering staging profiles — #701 +The first protected attempt built and activated signup, but the hosted signup test exposed missing explicit SDK project context. #703 repairs that exact partial release and the provider's empty no-retry representation. Partial deployment is not proof of a working profile service. Completed proof still requires both Functions and the hosted checks below; no permission widening or production fallback is allowed. + **Purpose:** prove only profile creation and recovery using made-up staging accounts. This is not membership approval, event management, a payment release or a public website launch. **Approver:** Dave Liu, sole release/recovery owner. Administrator bypass is disabled; normal self-approval remains permitted. **Availability:** **NOT AVAILABLE YET** until #701 records exact merged source, protected deployment, actual settings and hosted checks. #113/#133/#136 remain unresolved for production; this isolated engineering proof does not waive them. diff --git a/scripts/staging-profile-release.js b/scripts/staging-profile-release.js index 4508c9f..3344a91 100644 --- a/scripts/staging-profile-release.js +++ b/scripts/staging-profile-release.js @@ -78,14 +78,14 @@ function functionSpec(target, sourceSha, bundleDigest, uploadUrl) { sourceUploadUrl: uploadUrl, availableMemoryMb: 256, timeout: '30s', minInstances: 0, maxInstances: 2, ingressSettings: 'ALLOW_ALL', labels: { 'managed-by': 'mprc-profile-stage', 'source-sha': sourceSha, 'bundle-a': bundleDigest.slice(0, 32), 'bundle-b': bundleDigest.slice(32) }, - environmentVariables: { FIREBASE_CONFIG: JSON.stringify({ projectId: PROJECT }), ENFORCE_APP_CHECK: 'true', COMMERCE_ENABLED: 'false' }, + environmentVariables: { FIREBASE_CONFIG: JSON.stringify({ projectId: PROJECT }), GCLOUD_PROJECT: PROJECT, ENFORCE_APP_CHECK: 'true', COMMERCE_ENABLED: 'false' }, ...(target === 'ensureMemberProfile' ? { httpsTrigger: { securityLevel: 'SECURE_ALWAYS' } } : { eventTrigger: { eventType: 'providers/firebase.auth/eventTypes/user.create', resource: `projects/${PROJECT}`, service: 'firebaseauth.googleapis.com' } }), }; } -function validateHostedFunction(actual, expected, { ready = true } = {}) { +function validateHostedFunction(actual, expected, { ready = true, previousInitialRelease = false } = {}) { check(actual && actual.name === expected.name); for (const field of ['entryPoint', 'runtime', 'serviceAccountEmail', 'buildServiceAccount', 'dockerRepository', 'availableMemoryMb', 'timeout', 'maxInstances', 'ingressSettings']) check(actual[field] === expected[field]); // Protobuf omits a configured numeric zero, but not other required settings. @@ -95,15 +95,29 @@ function validateHostedFunction(actual, expected, { ready = true } = {}) { check(actual.status === 'ACTIVE'); for (const [key, value] of Object.entries(expected.labels)) check(actual.labels[key] === value); } - check(JSON.stringify(actual.environmentVariables) === JSON.stringify(expected.environmentVariables) - || Object.keys(actual.environmentVariables || {}).length === Object.keys(expected.environmentVariables).length - && Object.entries(expected.environmentVariables).every(([key, value]) => actual.environmentVariables[key] === value)); + // Only the exact partial #702 signup release may lack the SDK project + // context. Admit it for same-bundle repair, never as completed proof. + const acceptedEnvironment = !ready && previousInitialRelease + && actual.name.endsWith('/createMemberOnSignUp') + && actual.labels?.['source-sha'] === '3edffdf51ccef248d83e2c5c8600c3628d2e0ae1' + && actual.labels?.['bundle-a'] === expected.labels['bundle-a'] + && actual.labels?.['bundle-b'] === expected.labels['bundle-b'] + && actual.environmentVariables?.GCLOUD_PROJECT === undefined + ? Object.fromEntries(Object.entries(expected.environmentVariables).filter(([key]) => key !== 'GCLOUD_PROJECT')) + : expected.environmentVariables; + check(Object.keys(actual.environmentVariables || {}).length === Object.keys(acceptedEnvironment).length + && Object.entries(acceptedEnvironment).every(([key, value]) => actual.environmentVariables[key] === value)); check(!actual.secretEnvironmentVariables?.length && !actual.secretVolumes?.length && !actual.vpcConnector); if (expected.httpsTrigger) { check(actual.httpsTrigger?.securityLevel === 'SECURE_ALWAYS' && !actual.eventTrigger); } else { check(actual.eventTrigger?.eventType === expected.eventTrigger.eventType && actual.eventTrigger?.resource === expected.eventTrigger.resource && !actual.httpsTrigger); - check(!actual.eventTrigger.failurePolicy); // Same non-retrying background source behavior. + // The v1 service also returns an empty FailurePolicy message for no retry. + // A present retry message, including retry: {}, means retry is enabled. + const failurePolicy = actual.eventTrigger.failurePolicy; + check(failurePolicy === undefined || (failurePolicy !== null + && typeof failurePolicy === 'object' && !Array.isArray(failurePolicy) + && Object.keys(failurePolicy).length === 0)); } } @@ -144,7 +158,7 @@ async function release(env) { check(Array.isArray(existing) && existing.length <= 2); for (const fn of existing) { const target = fn.name?.split('/').at(-1); - validateHostedFunction(fn, functionSpec(target, env.GITHUB_SHA, bundle.digest, 'https://storage.googleapis.com/synthetic'), { ready: false }); + validateHostedFunction(fn, functionSpec(target, env.GITHUB_SHA, bundle.digest, 'https://storage.googleapis.com/synthetic'), { ready: false, previousInitialRelease: true }); } for (const target of TARGETS) { await verifyCurrentSource(env); // Recheck immediately before each write. diff --git a/tests/staging-profile-authority.test.js b/tests/staging-profile-authority.test.js index 43e1d54..e661cc6 100644 --- a/tests/staging-profile-authority.test.js +++ b/tests/staging-profile-authority.test.js @@ -215,6 +215,8 @@ test('two first-generation specs use explicit identities, limits and no secrets/ assert.match(spec.serviceAccountEmail, /^mprc-profile-runtime@run-mprc-staging\./); assert.match(spec.buildServiceAccount, /^projects\/run-mprc-staging\/serviceAccounts\/mprc-profile-build@/); assert.equal(spec.environmentVariables.COMMERCE_ENABLED, 'false'); + assert.equal(spec.environmentVariables.GCLOUD_PROJECT, 'run-mprc-staging'); + assert.equal(JSON.parse(spec.environmentVariables.FIREBASE_CONFIG).projectId, 'run-mprc-staging'); assert.ok(!spec.secretEnvironmentVariables && !spec.secretVolumes); if (target === 'ensureMemberProfile') assert.equal(spec.httpsTrigger.securityLevel, 'SECURE_ALWAYS'); else assert.equal(spec.eventTrigger.resource, 'projects/run-mprc-staging'); @@ -234,7 +236,13 @@ test('hosted resource readback rejects broad identities, foreign config, missing for (const [field, value] of Object.entries({ runtime: 'nodejs20', serviceAccountEmail: 'default', buildServiceAccount: 'default', minInstances: 1, maxInstances: undefined, availableMemoryMb: 512, timeout: '540s', status: 'DEPLOY_IN_PROGRESS', secretEnvironmentVariables: [{}], vpcConnector: 'foreign', labels: {}, environmentVariables: { ...expected.environmentVariables, COMMERCE_ENABLED: 'true' } })) { assert.throws(() => release.validateHostedFunction({ ...actual, [field]: value }, expected)); } - if (target === 'createMemberOnSignUp') assert.throws(() => release.validateHostedFunction({ ...actual, eventTrigger: { ...expected.eventTrigger, resource: 'projects/production' } }, expected)); + if (target === 'createMemberOnSignUp') { + release.validateHostedFunction({ ...actual, eventTrigger: { ...expected.eventTrigger, failurePolicy: {} } }, expected); + for (const failurePolicy of [{ retry: {} }, { unexpected: true }, [], null]) { + assert.throws(() => release.validateHostedFunction({ ...actual, eventTrigger: { ...expected.eventTrigger, failurePolicy } }, expected)); + } + assert.throws(() => release.validateHostedFunction({ ...actual, eventTrigger: { ...expected.eventTrigger, resource: 'projects/production' } }, expected)); + } } }); @@ -247,6 +255,20 @@ test('runtime/build actAs is checked on exact accounts without accepting key cre await assert.rejects(release.validateActAs('synthetic-token', mockFetch([{ permissions: ['iam.serviceAccounts.actAs', 'iam.serviceAccountKeys.create'] }]))); }); +test('repair admission is limited to exact initial signup source and bundle, never hosted success', () => { + const expected = expectedFunction('createMemberOnSignUp'); + const actual = { ...expected, status: 'ACTIVE', labels: { ...expected.labels, 'source-sha': '3edffdf51ccef248d83e2c5c8600c3628d2e0ae1' }, environmentVariables: { ...expected.environmentVariables }, eventTrigger: { ...expected.eventTrigger, failurePolicy: {} } }; + delete actual.environmentVariables.GCLOUD_PROJECT; + release.validateHostedFunction(actual, expected, { ready: false, previousInitialRelease: true }); + assert.throws(() => release.validateHostedFunction(actual, expected)); + assert.throws(() => release.validateHostedFunction(actual, expected, { ready: false })); + for (const [key, value] of Object.entries({ 'source-sha': 'c'.repeat(40), 'bundle-a': 'c'.repeat(32), 'bundle-b': 'c'.repeat(32) })) { + assert.throws(() => release.validateHostedFunction({ ...actual, labels: { ...actual.labels, [key]: value } }, expected, { ready: false, previousInitialRelease: true })); + } + assert.throws(() => release.validateHostedFunction({ ...actual, environmentVariables: { ...actual.environmentVariables, GCLOUD_PROJECT: 'production' } }, expected, { ready: false, previousInitialRelease: true })); + assert.throws(() => release.validateHostedFunction({ ...actual, environmentVariables: { ...actual.environmentVariables, STRIPE_SECRET_KEY: 'synthetic' } }, expected, { ready: false, previousInitialRelease: true })); +}); + test('operation polling is bounded, and failed or malformed operations never count as success', async () => { let requests = 0; await release.waitForOperation({ name: 'operations/synthetic' }, 'synthetic-token', async () => ({ done: ++requests === 2 }), async () => {});