diff --git a/Cargo.lock b/Cargo.lock index 893f835..de8cac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1888,7 +1888,7 @@ dependencies = [ [[package]] name = "locks-server" -version = "0.1.0" +version = "0.1.1-rc1" dependencies = [ "anyhow", "async-trait", @@ -2511,7 +2511,7 @@ dependencies = [ "url", "wasm-bindgen-futures", "web-time", - "webpki-roots 1.0.9", + "webpki-roots 1.0.10", "zeroize", ] @@ -2959,9 +2959,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -4263,9 +4263,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] @@ -4276,14 +4276,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.10", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] diff --git a/examples/js-sdk/package.json b/examples/js-sdk/package.json index 1cca773..ccb68ff 100644 --- a/examples/js-sdk/package.json +++ b/examples/js-sdk/package.json @@ -24,6 +24,7 @@ "test:pubky-helper": "node scripts/test-pubky-helper.mjs", "test:creator-session-state": "node scripts/test-creator-session-state.mjs", "test:pubky": "node scripts/test-pubky.mjs", + "test:release-version-contract": "node scripts/test-release-version-contract.mjs", "start-server": "node scripts/start-demo-server.mjs", "start-reader-server": "node scripts/start-reader-demo-server.mjs", "init-paykit-compose": "node scripts/init-paykit-compose.mjs", @@ -34,9 +35,9 @@ "validate:paykit-compose": "node scripts/validate-paykit-compose.mjs", "validate:staging-compose": "node scripts/validate-staging-compose.mjs", "check:paykit-setup-contract": "node scripts/check-paykit-setup-contract.mjs", - "smoke:paykit-compose": "npm run validate:paykit-compose && npm run check:paykit-setup-contract && npm run test:pubky && npm run test:paykit-reader-worker && node scripts/smoke-paykit-compose.mjs", + "smoke:paykit-compose": "npm run validate:paykit-compose && npm run check:paykit-setup-contract && npm run test:release-version-contract && npm run test:pubky && npm run test:paykit-reader-worker && node scripts/smoke-paykit-compose.mjs", "smoke": "npm --prefix ../../locks-sdk/bindings/js run smoke:examples", - "check": "node --check scripts/test-reader-load-state.mjs && node --check reader-load-state.js && node --check scripts/test-pubky.mjs && node --check scripts/init-config.mjs && node --check scripts/init-staging-config.mjs && node --check scripts/lib/staging-config.mjs && node --check scripts/lib/demo-runtime.mjs && node --check scripts/test-staging-config.mjs && node --check scripts/validate-staging-compose.mjs && node --check scripts/test-staging-compose.mjs && node --check scripts/test-staging-creator-mode.mjs && node --check scripts/test-staging-reader-mode.mjs && node --check scripts/test-reader-persistence.mjs && node --check scripts/test-connection-state-indicator.mjs && node --check scripts/test-pubky-helper.mjs && node --check scripts/test-creator-session-state.mjs && node --check scripts/reset-paykit-staging-demo-local.mjs && node --check scripts/test-reset-paykit-staging-demo-local.mjs && node --check scripts/init-paykit-compose.mjs && node --check scripts/homegate-bridge.mjs && node --check scripts/generate-paykit-account-tpub.mjs && node --check scripts/reset-paykit-demo.mjs && node --check scripts/validate-paykit-compose.mjs && node --check scripts/check-paykit-setup-contract.mjs && node --check scripts/electrum-readiness.mjs && node --check scripts/smoke-paykit-compose.mjs && node --check scripts/test-paykit-reader-worker.mjs && node --check scripts/create-user.mjs && node --check scripts/publish-creator-profile.mjs && node --check scripts/authenticate.mjs && node --check scripts/authenticate-paykit.mjs && node --check scripts/register-paykit-reader.mjs && node --check scripts/prepare-paykit-reader.mjs && node --check scripts/receive-paykit-request.mjs && node --check scripts/lib/creator-session-state.mjs && node --check scripts/lib/creator-static-path.mjs && node --check scripts/lib/paykit-reader-helper.mjs && node --check scripts/lib/paykit-reader-status.mjs && node --check scripts/lib/paykit-reader-worker.mjs && node --check scripts/start-demo-server.mjs && node --check scripts/start-reader-demo-server.mjs && node --check demo-network.js && node --check reader-persistence.js && node --check reader-staging-paykit.js && node --check app.js && node --check app-iframe.js && node --check creator-identity.js && node --check creator-lock-policy.js && node --check payment-amount.js && node --check paykit-setup.js && node --check creator-complete-flow.js && node --check reader-app.js && node --check reader-flow.js" + "check": "node --check scripts/test-reader-load-state.mjs && node --check reader-load-state.js && node --check scripts/test-pubky.mjs && node --check scripts/test-release-version-contract.mjs && node --check scripts/lib/release-version-contract.mjs && node --check scripts/init-config.mjs && node --check scripts/init-staging-config.mjs && node --check scripts/lib/staging-config.mjs && node --check scripts/lib/demo-runtime.mjs && node --check scripts/test-staging-config.mjs && node --check scripts/validate-staging-compose.mjs && node --check scripts/test-staging-compose.mjs && node --check scripts/test-staging-creator-mode.mjs && node --check scripts/test-staging-reader-mode.mjs && node --check scripts/test-reader-persistence.mjs && node --check scripts/test-connection-state-indicator.mjs && node --check scripts/test-pubky-helper.mjs && node --check scripts/test-creator-session-state.mjs && node --check scripts/reset-paykit-staging-demo-local.mjs && node --check scripts/test-reset-paykit-staging-demo-local.mjs && node --check scripts/init-paykit-compose.mjs && node --check scripts/homegate-bridge.mjs && node --check scripts/generate-paykit-account-tpub.mjs && node --check scripts/reset-paykit-demo.mjs && node --check scripts/validate-paykit-compose.mjs && node --check scripts/check-paykit-setup-contract.mjs && node --check scripts/electrum-readiness.mjs && node --check scripts/smoke-paykit-compose.mjs && node --check scripts/test-paykit-reader-worker.mjs && node --check scripts/create-user.mjs && node --check scripts/publish-creator-profile.mjs && node --check scripts/authenticate.mjs && node --check scripts/authenticate-paykit.mjs && node --check scripts/register-paykit-reader.mjs && node --check scripts/prepare-paykit-reader.mjs && node --check scripts/receive-paykit-request.mjs && node --check scripts/lib/creator-session-state.mjs && node --check scripts/lib/creator-static-path.mjs && node --check scripts/lib/paykit-reader-helper.mjs && node --check scripts/lib/paykit-reader-status.mjs && node --check scripts/lib/paykit-reader-worker.mjs && node --check scripts/start-demo-server.mjs && node --check scripts/start-reader-demo-server.mjs && node --check demo-network.js && node --check reader-persistence.js && node --check reader-staging-paykit.js && node --check app.js && node --check app-iframe.js && node --check creator-identity.js && node --check creator-lock-policy.js && node --check payment-amount.js && node --check paykit-setup.js && node --check creator-complete-flow.js && node --check reader-app.js && node --check reader-flow.js" }, "dependencies": { "@synonymdev/pubky": "^0.16.2" diff --git a/examples/js-sdk/scripts/lib/release-version-contract.mjs b/examples/js-sdk/scripts/lib/release-version-contract.mjs new file mode 100644 index 0000000..9a60f83 --- /dev/null +++ b/examples/js-sdk/scripts/lib/release-version-contract.mjs @@ -0,0 +1,8 @@ +export function assertCargoLockPackageVersions(cargoLockSource, expectedVersions) { + for (const [packageName, expectedVersion] of expectedVersions) { + const packageEntry = `name = "${packageName}"\nversion = "${expectedVersion}"`; + if (!cargoLockSource.includes(packageEntry)) { + throw new Error(`${packageName} Cargo.lock version must be ${expectedVersion}`); + } + } +} diff --git a/examples/js-sdk/scripts/smoke-paykit-compose.mjs b/examples/js-sdk/scripts/smoke-paykit-compose.mjs index aba9ae9..3fb7ac7 100644 --- a/examples/js-sdk/scripts/smoke-paykit-compose.mjs +++ b/examples/js-sdk/scripts/smoke-paykit-compose.mjs @@ -25,10 +25,12 @@ import { DEFAULT_PAYKIT_SERVER_CONTEXT, PAYKIT_SERVER_REF, } from './lib/paykit-server-source.mjs'; +import { assertCargoLockPackageVersions } from './lib/release-version-contract.mjs'; const lockServerPubky = 'pubky7ir1ttte48bcp4zjychjyscicrwi1j34mtt91ptsafdbjmr8g9eo'; const creatorPubky = 'pubkytkrq8zmwb8a3m9k15csu3q17qmfgqnp9dskbrg9uq1rydpyxp7qy'; const locksReleaseVersion = '0.1.0'; +const locksServerReleaseVersion = '0.1.1-rc1'; assert.equal(PAYKIT_SERVER_REF, '4cc8e7b836dba28446a8d821f593ee40bb4acbeb'); assert.equal( DEFAULT_PAYKIT_SERVER_CONTEXT, @@ -89,20 +91,14 @@ assert.match(composeSource, /^# Local development and demonstration only;/); assert.match(composeSource, /^name: pubky-locks-paykit-demo$/m); assert.match(cargoManifestSource, new RegExp(`^version = "${locksReleaseVersion}"$`, 'm')); assert.equal(jsPackage.version, locksReleaseVersion); -for (const packageName of [ - 'locks-core', - 'locks-e2e', - 'locks-sdk', - 'locks-sdk-wasm', - 'locks-server', - 'locks-service', -]) { - assert.match( - cargoLockSource, - new RegExp(`name = "${packageName}"\\nversion = "${locksReleaseVersion}"`), - `${packageName} Cargo.lock version must match the release candidate`, - ); -} +assertCargoLockPackageVersions(cargoLockSource, new Map([ + ['locks-core', locksReleaseVersion], + ['locks-e2e', locksReleaseVersion], + ['locks-sdk', locksReleaseVersion], + ['locks-sdk-wasm', locksReleaseVersion], + ['locks-server', locksServerReleaseVersion], + ['locks-service', locksReleaseVersion], +])); assert.match(creatorAppSource, /hasExactKeys\(event\.data, \['type', 'state', 'code'\]\)/); assert.match(creatorAppSource, /body: JSON\.stringify\(\{ level \}\)/); assert.match(creatorAppSource, /\[result\.authorizationUrl, result\.command\]\.filter\(Boolean\)/); @@ -590,7 +586,7 @@ assert.ok( ); assert.equal( packageJson.scripts['smoke:paykit-compose'], - 'npm run validate:paykit-compose && npm run check:paykit-setup-contract && npm run test:pubky && npm run test:paykit-reader-worker && node scripts/smoke-paykit-compose.mjs', + 'npm run validate:paykit-compose && npm run check:paykit-setup-contract && npm run test:release-version-contract && npm run test:pubky && npm run test:paykit-reader-worker && node scripts/smoke-paykit-compose.mjs', ); console.log('Paykit Compose smoke check passed'); diff --git a/examples/js-sdk/scripts/test-release-version-contract.mjs b/examples/js-sdk/scripts/test-release-version-contract.mjs new file mode 100644 index 0000000..f74911b --- /dev/null +++ b/examples/js-sdk/scripts/test-release-version-contract.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; + +import { assertCargoLockPackageVersions } from './lib/release-version-contract.mjs'; + +const expectedVersions = new Map([ + ['locks-core', '0.1.0'], + ['locks-server', '0.1.1-rc1'], +]); +const currentCargoLock = `[[package]] +name = "locks-core" +version = "0.1.0" + +[[package]] +name = "locks-server" +version = "0.1.1-rc1" +`; + +assert.doesNotThrow(() => assertCargoLockPackageVersions(currentCargoLock, expectedVersions)); + +const staleCargoLock = currentCargoLock.replace('version = "0.1.1-rc1"', 'version = "0.1.0"'); +assert.throws( + () => assertCargoLockPackageVersions(staleCargoLock, expectedVersions), + /locks-server Cargo.lock version must be 0\.1\.1-rc1/, +); + +console.log('Release version contract tests passed'); diff --git a/locks-server/Cargo.toml b/locks-server/Cargo.toml index dd3c3c9..6e28296 100644 --- a/locks-server/Cargo.toml +++ b/locks-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "locks-server" -version.workspace = true +version = "0.1.1-rc1" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/locks-server/src/config/loading.rs b/locks-server/src/config/loading.rs index c8e1013..5a89b9a 100644 --- a/locks-server/src/config/loading.rs +++ b/locks-server/src/config/loading.rs @@ -127,6 +127,26 @@ fn initialize_default_config( service_home: PathBuf, secret_path: PathBuf, identity_provider: &impl LockServerIdentityProvider, +) -> Result { + initialize_default_config_with_database_url( + config_path, + service_home, + secret_path, + identity_provider, + || { + std::env::var("PUBKY_LOCK_DATABASE_URL").map_err(|_| { + ConfigError::MissingDatabaseUrlEnv("PUBKY_LOCK_DATABASE_URL".to_owned()) + }) + }, + ) +} + +fn initialize_default_config_with_database_url( + config_path: PathBuf, + service_home: PathBuf, + secret_path: PathBuf, + identity_provider: &impl LockServerIdentityProvider, + database_url: impl FnOnce() -> Result, ) -> Result { std::fs::create_dir_all(&service_home).map_err(|source| ConfigError::CreateServiceHome { path: service_home.clone(), @@ -134,6 +154,7 @@ fn initialize_default_config( })?; let public_key = identity_provider.generate_secret(&secret_path)?; + let database_url = database_url()?; let config = LockServerRuntimeConfig { bind_addr: "127.0.0.1:3000".parse().expect("static bind addr is valid"), credentials: LockServerCredentialsConfig { @@ -142,9 +163,7 @@ fn initialize_default_config( max_ttl_seconds: 900, }, database: DatabaseConfig { - url: std::env::var("PUBKY_LOCK_DATABASE_URL").map_err(|_| { - ConfigError::MissingDatabaseUrlEnv("PUBKY_LOCK_DATABASE_URL".to_owned()) - })?, + url: database_url, max_connections: 10, run_migrations_on_startup: true, }, @@ -355,19 +374,24 @@ mod tests { use crate::config::{FilesystemLockServerIdentityProvider, GrantConnectAcquisitionConfig}; use tempfile::tempdir; + const TEST_DATABASE_URL: &str = "postgres://localhost/locks_test"; + + fn initialize_test_default_config(home: &Path) -> LockServerRuntimeConfig { + let service_home = home.join(DEFAULT_SERVICE_HOME); + initialize_default_config_with_database_url( + service_home.join(DEFAULT_CONFIG_FILE), + service_home.clone(), + service_home.join(DEFAULT_SECRET_FILE), + &FilesystemLockServerIdentityProvider, + || Ok(TEST_DATABASE_URL.to_owned()), + ) + .unwrap() + } + #[test] fn generated_default_config_comments_every_setting_and_remains_parseable() { let home = tempdir().unwrap(); - unsafe { - std::env::set_var( - "PUBKY_LOCK_DATABASE_URL", - "postgres://locks:locks@localhost/locks_test", - ); - } - - let generated = - load_or_initialize_config(None, home.path(), &FilesystemLockServerIdentityProvider) - .unwrap(); + let generated = initialize_test_default_config(home.path()); let config_path = home.path().join(".pubky-lock/config.toml"); let config_text = std::fs::read_to_string(&config_path).unwrap(); @@ -375,6 +399,14 @@ mod tests { assert!(line.contains(" # "), "missing inline comment: {line}"); } + std::fs::write( + &config_path, + config_text.replace( + "url_env = \"PUBKY_LOCK_DATABASE_URL\"", + &format!("url = \"{TEST_DATABASE_URL}\""), + ), + ) + .unwrap(); let parsed = load_existing_config_from_path(&config_path).unwrap(); assert_eq!(parsed, generated); @@ -394,15 +426,7 @@ mod tests { #[test] fn paykit_config_requires_signing_seed_secret_on_startup() { let home = tempdir().unwrap(); - unsafe { - std::env::set_var( - "PUBKY_LOCK_DATABASE_URL", - "postgres://locks:locks@localhost/locks_test", - ); - } - let generated = - load_or_initialize_config(None, home.path(), &FilesystemLockServerIdentityProvider) - .unwrap(); + let generated = initialize_test_default_config(home.path()); let secret_path = home.path().join(".pubky-lock/secret.sess"); std::fs::write( &secret_path, @@ -413,6 +437,16 @@ mod tests { ) .unwrap(); + let config_path = home.path().join(".pubky-lock/config.toml"); + let config_text = std::fs::read_to_string(&config_path).unwrap(); + std::fs::write( + &config_path, + config_text.replace( + "url_env = \"PUBKY_LOCK_DATABASE_URL\"", + &format!("url = \"{TEST_DATABASE_URL}\""), + ), + ) + .unwrap(); let error = load_or_initialize_config(None, home.path(), &FilesystemLockServerIdentityProvider) .unwrap_err(); @@ -423,15 +457,7 @@ mod tests { #[test] fn grant_connect_requires_signing_seed_secret_on_startup() { let home = tempdir().unwrap(); - unsafe { - std::env::set_var( - "PUBKY_LOCK_DATABASE_URL", - "postgres://locks:***@localhost/locks_test", - ); - } - let mut generated = - load_or_initialize_config(None, home.path(), &FilesystemLockServerIdentityProvider) - .unwrap(); + let mut generated = initialize_test_default_config(home.path()); generated.paykit = None; generated.creator_authority_acquisition.method = CreatorAuthorityAcquisitionMethod::GrantConnect; diff --git a/locks-server/src/pkdns.rs b/locks-server/src/pkdns.rs index 0ec839c..6df1c1a 100644 --- a/locks-server/src/pkdns.rs +++ b/locks-server/src/pkdns.rs @@ -92,6 +92,7 @@ async fn run_periodic( interval.tick().await; loop { tokio::select! { + biased; changed = shutdown.changed() => { if changed.is_err() || *shutdown.borrow() { return; @@ -223,29 +224,32 @@ mod tests { #[tokio::test] async fn failed_periodic_publication_is_retried_until_shutdown() { - let attempts = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&attempts); - let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); - - run_periodic( - std::time::Duration::from_millis(1), - shutdown_rx, - move || { - let attempt = observed.fetch_add(1, Ordering::SeqCst); - let shutdown_tx = shutdown_tx.clone(); - async move { - if attempt == 0 { - Err("relay unavailable") - } else { - shutdown_tx.send(true).unwrap(); - Ok(()) + for _ in 0..20 { + let attempts = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&attempts); + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + + run_periodic( + std::time::Duration::from_millis(1), + shutdown_rx, + move || { + let attempt = observed.fetch_add(1, Ordering::SeqCst); + let shutdown_tx = shutdown_tx.clone(); + async move { + if attempt == 0 { + Err("relay unavailable") + } else { + shutdown_tx.send(true).unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(2)).await; + Ok(()) + } } - } - }, - ) - .await; + }, + ) + .await; - assert_eq!(attempts.load(Ordering::SeqCst), 2); + assert_eq!(attempts.load(Ordering::SeqCst), 2); + } } #[test]