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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VISION_MOBILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The following are preferred architectural approaches in service of the experienc

- We want to minimize the average time elapsed between (a) merging a change and (b) the change appearing in Comp Portal and the app stores.
- We should always have a release `Waiting for Review` / `In Review` in the iOS App Store. Immediately a release is approved, we submit a new one.
- We should build and upload release candidates frequently. Every RC should go to Comp Portal and TestFlight. Building and distributing an RC from a commit on main should require only a single click/command. When the time arrives to submit the next release to public app stores, there should already be a suitable RC uploaded and dogfooded.
- We should build and upload release candidates frequently. Every RC should go to Comp Portal and TestFlight. Building and distributing an RC from a commit on main should require only a single click/command. When the time arrives to submit the next release to public app stores, there should already be a suitable RC uploaded and tested.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nonblocking: this wording change, and the age-gate provider/test/README edits, just swap testing terminology and don't seem related to removing app profiles. fine to keep if intentional, but they'd be cleaner as a separate PR


## Critical functionality

Expand Down
58 changes: 56 additions & 2 deletions crates/buzz-db/src/runtime/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,9 +705,14 @@ mod postgres_tests {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);

assert_eq!(migrations.len(), 59);
assert_eq!(migrations.len(), 60);
assert_eq!(migrations[58].version, 59);
assert!(migrations[58].sql.as_str().contains("buzz.push_enabled"));
assert_eq!(migrations[59].version, 60);
assert!(migrations[59]
.sql
.as_str()
.contains("ALTER TABLE push_leases DROP COLUMN app_profile"));
assert_eq!(migrations[55].version, 56);
assert!(migrations[55]
.sql
Expand Down Expand Up @@ -1329,7 +1334,7 @@ mod postgres_tests {
assert!(dogfood_profile
.contains("DROP CONSTRAINT push_gateway_installations_app_profile_check"));
assert!(dogfood_profile.contains("CHECK (app_profile = 'buzz-ios-dogfood')"));
assert!(desired_schema.contains("CHECK (app_profile = 'buzz-ios-dogfood')"));
assert!(!desired_schema.contains("app_profile"));

// Drop the Phase-A NIP-FI relay-side authority ledger (0041 + 0042).
// OSS Buzz is stateless for identity (spec v2, PR #7214); the durable
Expand Down Expand Up @@ -3258,4 +3263,53 @@ mod postgres_tests {
);
}
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn migration_0060_refuses_active_legacy_leases_without_mutation() {
let pool = connect_test_pool().await;
reset_public_schema(&pool).await;
run_migrations_through(&pool, 59)
.await
.expect("legacy schema");
let community = uuid::Uuid::new_v4();
sqlx::query("INSERT INTO communities(id,host) VALUES($1,'push-cutover.example')")
.bind(community)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO push_leases(community_id,author,installation_id,source_event_id,source_created_at,generation,active,app_profile,endpoint_hash,endpoint_grant,max_class,subscriptions,expires_at) VALUES($1,$2,'legacy',$3,1,1,true,'buzz-ios-dogfood',$4,'opaque','default','[]',2000000000)")
.bind(community).bind(vec![1_u8;32]).bind(vec![2_u8;32]).bind(vec![3_u8;32])
.execute(&pool).await.unwrap();
let error = run_migrations(&pool)
.await
.expect_err("active legacy lease blocks cutover");
assert!(
error.to_string().contains("no active legacy push leases"),
"{error}"
);
let preserved: (bool, String) =
sqlx::query_as("SELECT active,app_profile FROM push_leases WHERE community_id=$1")
.bind(community)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(preserved, (true, "buzz-ios-dogfood".to_owned()));
sqlx::query("UPDATE push_leases SET active=false,app_profile=NULL,endpoint_hash=NULL,endpoint_grant=NULL,max_class=NULL,subscriptions=NULL WHERE community_id=$1")
.bind(community).execute(&pool).await.unwrap();
run_migrations(&pool)
.await
.expect("inactive legacy lease permits cutover");
let count: i64 = sqlx::query_scalar(
"SELECT count(*) FROM push_leases WHERE community_id=$1 AND NOT active",
)
.bind(community)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 1);
assert!(sqlx::query("SELECT app_profile FROM push_leases")
.execute(&pool)
.await
.is_err());
}
}
4 changes: 2 additions & 2 deletions crates/buzz-db/src/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,9 +819,9 @@ async fn push_gateway_profile_migration_converges_brownfield_authority() {
.await
.expect("insert delegation for legacy installation");

migration::run_migrations(&pool)
migration::run_migrations_through(&pool, 43)
.await
.expect("apply dogfood-only migration");
.expect("apply historical profile migration");

let legacy_installations: i64 =
sqlx::query_scalar("SELECT count(*) FROM push_gateway_installations")
Expand Down
5 changes: 2 additions & 3 deletions crates/buzz-db/src/store/event_follow_up_postgres_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ async fn activate_lease(
expires_at,
},
push::ActiveLease {
app_profile: "ios-production",
endpoint_hash: &endpoint_hash,
endpoint_grant: "test-grant",
max_class: "default",
Expand Down Expand Up @@ -1015,10 +1014,10 @@ async fn assert_async_isolation(db: &Db, community: CommunityId, keys: &Keys) {
.unwrap();
sqlx::query(
"INSERT INTO push_leases (community_id, author, installation_id, source_event_id, \
source_created_at, generation, active, endpoint_enabled, app_profile, endpoint_hash, \
source_created_at, generation, active, endpoint_enabled, endpoint_hash, \
endpoint_grant, max_class, subscriptions, expires_at, updated_at) \
SELECT $1, author, installation_id, source_event_id, source_created_at, generation, \
active, endpoint_enabled, app_profile, endpoint_hash, endpoint_grant, max_class, \
active, endpoint_enabled, endpoint_hash, endpoint_grant, max_class, \
subscriptions, expires_at, clock_timestamp() FROM push_leases WHERE community_id=$2",
)
.bind(later.as_uuid())
Expand Down
55 changes: 20 additions & 35 deletions crates/buzz-db/src/store/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ pub struct LeaseVersion<'a> {
/// Effective fields for an active APNs lease.
#[derive(Debug, Clone, Copy)]
pub struct ActiveLease<'a> {
/// Application profile selected from the executor descriptor.
pub app_profile: &'a str,
/// SHA-256 of the platform endpoint.
pub endpoint_hash: &'a [u8],
/// Opaque endpoint grant issued by the stateless gateway.
Expand Down Expand Up @@ -300,7 +298,7 @@ pub async fn accept_lease_event(
// uniqueness forever. The author lock makes this cleanup atomic with the
// subsequent author-wide checks and replacement.
sqlx::query(
"UPDATE push_leases SET active=false, endpoint_enabled=false, app_profile=NULL, \
"UPDATE push_leases SET active=false, endpoint_enabled=false, \
endpoint_hash=NULL, endpoint_grant=NULL, max_class=NULL, subscriptions=NULL, updated_at=now() \
WHERE community_id=$1 AND author=$2 AND active \
AND expires_at <= EXTRACT(EPOCH FROM now())::bigint",
Expand All @@ -323,12 +321,11 @@ pub async fn accept_lease_event(
return Ok(AcceptLeaseOutcome::LeaseQuotaExceeded);
}
let duplicate: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM push_leases WHERE community_id=$1 AND author=$2 AND installation_id<>$3 AND active AND app_profile=$4 AND endpoint_hash=$5)",
"SELECT EXISTS(SELECT 1 FROM push_leases WHERE community_id=$1 AND author=$2 AND installation_id<>$3 AND active AND endpoint_hash=$4)",
)
.bind(community.as_uuid())
.bind(author)
.bind(installation_id)
.bind(active.app_profile)
.bind(active.endpoint_hash)
.fetch_one(tx.conn())
.await?;
Expand Down Expand Up @@ -375,11 +372,10 @@ pub async fn accept_lease_event(
)
.await?;

let (is_active, app_profile, endpoint_hash, endpoint_grant, max_class, subscriptions) = active
.map_or((false, None, None, None, None, None), |active| {
let (is_active, endpoint_hash, endpoint_grant, max_class, subscriptions) = active
.map_or((false, None, None, None, None), |active| {
(
true,
Some(active.app_profile),
Some(active.endpoint_hash),
Some(active.endpoint_grant),
Some(active.max_class),
Expand All @@ -388,18 +384,18 @@ pub async fn accept_lease_event(
});
if let Err(error) = sqlx::query(
r#"INSERT INTO push_leases (community_id,author,installation_id,source_event_id,
source_created_at,generation,active,app_profile,endpoint_hash,endpoint_grant,max_class,
subscriptions,expires_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
source_created_at,generation,active,endpoint_hash,endpoint_grant,max_class,
subscriptions,expires_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
ON CONFLICT (community_id,author,installation_id) DO UPDATE SET
source_event_id=EXCLUDED.source_event_id, source_created_at=EXCLUDED.source_created_at,
generation=EXCLUDED.generation, active=EXCLUDED.active, endpoint_enabled=true,
app_profile=EXCLUDED.app_profile, endpoint_hash=EXCLUDED.endpoint_hash,
endpoint_hash=EXCLUDED.endpoint_hash,
endpoint_grant=EXCLUDED.endpoint_grant, max_class=EXCLUDED.max_class,
subscriptions=EXCLUDED.subscriptions, expires_at=EXCLUDED.expires_at, updated_at=now()"#,
)
.bind(community.as_uuid()).bind(author).bind(installation_id)
.bind(version.source_event_id).bind(version.source_created_at).bind(version.generation)
.bind(is_active).bind(app_profile).bind(endpoint_hash).bind(endpoint_grant)
.bind(is_active).bind(endpoint_hash).bind(endpoint_grant)
.bind(max_class).bind(subscriptions).bind(version.expires_at)
.execute(tx.conn()).await
{
Expand Down Expand Up @@ -474,18 +470,16 @@ async fn replace_lease(
version: LeaseVersion<'_>,
active: Option<ActiveLease<'_>>,
) -> Result<ReplaceLeaseOutcome> {
let (is_active, app_profile, endpoint_hash, endpoint_grant, max_class, subscriptions) =
match active {
Some(active) => (
true,
Some(active.app_profile),
Some(active.endpoint_hash),
Some(active.endpoint_grant),
Some(active.max_class),
Some(active.subscriptions),
),
None => (false, None, None, None, None, None),
};
let (is_active, endpoint_hash, endpoint_grant, max_class, subscriptions) = match active {
Some(active) => (
true,
Some(active.endpoint_hash),
Some(active.endpoint_grant),
Some(active.max_class),
Some(active.subscriptions),
),
None => (false, None, None, None, None),
};

// T1b: an activating replacement can flip the community from "no eligible
// lease" to "eligible"; serialize it against the event producer's shared
Expand All @@ -505,16 +499,15 @@ async fn replace_lease(
r#"
INSERT INTO push_leases (
community_id, author, installation_id, source_event_id,
source_created_at, generation, active, app_profile, endpoint_hash,
source_created_at, generation, active, endpoint_hash,
endpoint_grant, max_class, subscriptions, expires_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (community_id, author, installation_id) DO UPDATE SET
source_event_id = EXCLUDED.source_event_id,
source_created_at = EXCLUDED.source_created_at,
generation = EXCLUDED.generation,
active = EXCLUDED.active,
endpoint_enabled = true,
app_profile = EXCLUDED.app_profile,
endpoint_hash = EXCLUDED.endpoint_hash,
endpoint_grant = EXCLUDED.endpoint_grant,
max_class = EXCLUDED.max_class,
Expand All @@ -539,7 +532,6 @@ async fn replace_lease(
.bind(version.source_created_at)
.bind(version.generation)
.bind(is_active)
.bind(app_profile)
.bind(endpoint_hash)
.bind(endpoint_grant)
.bind(max_class)
Expand Down Expand Up @@ -1574,7 +1566,6 @@ mod postgres_tests {
installation,
version(generation as u8, generation * 10, generation),
ActiveLease {
app_profile: "ios-production",
endpoint_hash: endpoint,
endpoint_grant: "opaque-grant",
max_class: "default",
Expand Down Expand Up @@ -1609,7 +1600,6 @@ mod postgres_tests {
expires_at: 200,
},
Some(ActiveLease {
app_profile: "ios-production",
endpoint_hash: &endpoint,
endpoint_grant: "opaque-grant",
max_class: "not-a-class",
Expand Down Expand Up @@ -1663,7 +1653,6 @@ mod postgres_tests {
expires_at: 1,
},
Some(ActiveLease {
app_profile: "ios-production",
endpoint_hash: &endpoint,
endpoint_grant: "opaque-grant",
max_class: "default",
Expand All @@ -1689,7 +1678,6 @@ mod postgres_tests {
expires_at: i64::MAX / 2,
},
Some(ActiveLease {
app_profile: "ios-production",
endpoint_hash: &[43; 32],
endpoint_grant: "replacement-grant",
max_class: "default",
Expand Down Expand Up @@ -1738,7 +1726,6 @@ mod postgres_tests {
expires_at: 200,
},
ActiveLease {
app_profile: "ios-production",
endpoint_hash: &endpoint,
endpoint_grant: "opaque-grant",
max_class: "default",
Expand Down Expand Up @@ -1800,7 +1787,6 @@ mod postgres_tests {
"install",
version(3, 15, 99),
ActiveLease {
app_profile: "ios-production",
endpoint_hash: &endpoint,
endpoint_grant: "grant",
max_class: "default",
Expand Down Expand Up @@ -2789,7 +2775,6 @@ mod postgres_tests {
"install",
version(1, 10, 1),
ActiveLease {
app_profile: "ios-production",
endpoint_hash: &[92; 32],
endpoint_grant: "opaque-grant",
max_class: "default",
Expand Down
12 changes: 12 additions & 0 deletions crates/buzz-push-gateway/migrations/0006_single_application.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- The new capability starts with fresh installation authority. Do not silently
-- retain fingerprints made with the removed application-profile namespace.
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM push_gateway_installations) THEN
RAISE EXCEPTION 'buzz-push-v1 requires an empty gateway authority store. Stop old gateway replicas and provision a fresh dedicated database before upgrading.';
END IF;
END
$$;
ALTER TABLE push_gateway_installations DROP COLUMN app_profile;
CREATE UNIQUE INDEX push_gateway_installations_active_token
ON push_gateway_installations (token_fingerprint) WHERE revoked_at IS NULL;
6 changes: 3 additions & 3 deletions crates/buzz-push-gateway/src/apns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,12 @@ mod tests {
}

#[tokio::test]
#[ignore = "requires the exported dogfood Apple Push Services PEM"]
#[ignore = "requires the exported configured Apple Push Services PEM"]
async fn live_sandbox_probe_reports_literal_status_and_body() {
let cert_path = std::env::var("BUZZ_PUSH_LIVE_APNS_CERT_PATH")
.expect("set BUZZ_PUSH_LIVE_APNS_CERT_PATH to the dogfood identity PEM");
.expect("set BUZZ_PUSH_LIVE_APNS_CERT_PATH to the configured identity PEM");
let topic = std::env::var("BUZZ_PUSH_LIVE_APNS_TOPIC")
.expect("set BUZZ_PUSH_LIVE_APNS_TOPIC to the dogfood bundle id");
.expect("set BUZZ_PUSH_LIVE_APNS_TOPIC to the configured bundle id");
let identity = std::fs::read(cert_path).unwrap();
let transport =
ApnsTransport::certificate(&identity, topic, ApnsEnvironment::Sandbox).unwrap();
Expand Down
Loading
Loading