···5566## [Unreleased]
7788+## [0.4.5] - 2026-03-23
99+1010+### Added
1111+- Added P2P device linking v1 in [#106](https://github.com/tilesprivacy/tiles/pull/106).
1212+ - Works both online and in offline networks
1313+ - Utility Commands for device linking
1414+ - `tiles link enable` - creates the ticket and listens for an link requests
1515+ - `tiles link enable <ticket>`- Device that need to join will run this command with the ticket from the sender. **NOTE**: The ticket sharing is out-of-band.
1616+ - `tiles link list-peers` - Shows the info (DID, nickname etc) of the linked devices.
1717+ - `tiles link disable <DID>` - Unlinks a linked device
1818+1919+### Fixed
2020+- Fixed the permission issues while trying to update Tiles using `tiles update` in [$104](https://github.com/tilesprivacy/tiles/pull/104). This was due to new binary location is in `/usr/` instead of `~/.local/`. Running the internal script with `sudo` fixed it.
2121+822## [0.4.4] - 2026-03-16
9231024### Added
+7-7
scripts/bundler.sh
···2222cp "target/${TARGET}/${BINARY_NAME}" "${DIST_DIR}/tmp/"
23232424# flushing this folder, else the final zip will have previous app-server zips too (#84)
2525-# rm -rf "${SERVER_DIR}/stack_export_prod"
2525+rm -rf "${SERVER_DIR}/stack_export_prod"
26262727-# echo "🔒 Locking the venvstack...."
2727+echo "🔒 Locking the venvstack...."
28282929-# venvstacks lock server/stack/venvstacks.toml
2929+venvstacks lock server/stack/venvstacks.toml
30303131-# echo "🛠️ Building the venvstack...."
3131+echo "🛠️ Building the venvstack...."
32323333-# venvstacks build server/stack/venvstacks.toml
3333+venvstacks build server/stack/venvstacks.toml
34343535-# echo "📦 Publishing the venvstack...."
3535+echo "📦 Publishing the venvstack...."
36363737-# venvstacks publish --tag-outputs --output-dir ../stack_export_prod server/stack/venvstacks.toml
3737+venvstacks publish --tag-outputs --output-dir ../stack_export_prod server/stack/venvstacks.toml
38383939cp -r "${SERVER_DIR}" "${DIST_DIR}/tmp/"
4040
+1-1
tilekit/src/accounts.rs
···3535///
3636/// - `app`- The service for which Identity is made (for ex: tiles)
3737/// - `did` - The `Identity` of the service
3838-pub fn get_secret_key(app: &str, did: &Identity) -> Result<SecretKey> {
3838+pub fn get_secret_key(app: &str, did: &str) -> Result<SecretKey> {
3939 let entry = Entry::new(app, did)?;
4040 let mut bytes: [u8; 64] = [0u8; 64];
4141 let secret_pair = entry.get_secret()?;
+26-10
tiles/src/core/accounts.rs
···11//! Accounts
22// Stuff related to account and identity system
33use anyhow::{Result, anyhow};
44+use iroh::SecretKey;
45use rusqlite::{Connection, types::FromSqlError};
56use std::{
67 fmt::Display,
78 time::{SystemTime, UNIX_EPOCH},
89};
99-use tilekit::accounts::create_identity;
1010+use tilekit::accounts::{create_identity, get_secret_key};
1011use toml::Table;
1112use uuid::Uuid;
1213···2930 // root account, created in the system
3031 LOCAL,
31323232- // remote account but same previlege as your local account
3333- SELF,
3333+ // remote account
3434+ PEER,
3435}
35363637#[derive(Debug)]
···5051 let value_lower = value.to_lowercase();
5152 match value_lower.as_str() {
5253 "local" => Ok(ACCOUNT::LOCAL),
5353- "self" => Ok(ACCOUNT::SELF),
5454+ "peer" => Ok(ACCOUNT::PEER),
5455 _ => Err(AccountError {
5556 error: "Invalid account type".to_owned(),
5657 }),
···6162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6263 match self {
6364 Self::LOCAL => write!(f, "{}", String::from("local")),
6464- Self::SELF => write!(f, "{}", String::from("self")),
6565+ Self::PEER => write!(f, "{}", String::from("peer")),
6566 }
6667 }
6768}
···287288288289// TODO: We could add unique user_id constraints, but
289290// we will wait for it until we solve the sync part
290290-pub fn save_self_account_db(db_conn: &Connection, user_id: &str, nickname: &str) -> Result<()> {
291291+pub fn save_peer_account_db(db_conn: &Connection, user_id: &str, nickname: &str) -> Result<()> {
291292 let user = User {
292293 id: Uuid::now_v7(),
293294 user_id: String::from(user_id),
294295 username: String::from(nickname),
295295- account_type: ACCOUNT::SELF,
296296+ account_type: ACCOUNT::PEER,
296297 active_profile: false,
297298 root: false,
298299 created_at: SystemTime::now()
···331332332333fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> {
333334 let mut root_user_table = root_user_config.clone();
334334- match create_identity("tiles") {
335335+ let app_name = if cfg!(debug_assertions) {
336336+ "tiles_dev"
337337+ } else {
338338+ "tiles"
339339+ };
340340+ match create_identity(app_name) {
335341 Ok(did) => {
336342 root_user_table.insert("id".to_owned(), toml::Value::String(did));
337343 if let Some(nickname) = nickname {
···389395 Ok(_) => Ok(()),
390396 Err(err) => Err(anyhow!("Unable to unlink the peer due to {:?}", err)),
391397 }
398398+}
399399+400400+pub fn get_app_secret_key(did: &str) -> Result<SecretKey> {
401401+ let app_name = if cfg!(debug_assertions) {
402402+ "tiles_dev"
403403+ } else {
404404+ "tiles"
405405+ };
406406+ let signing_key = get_secret_key(app_name, did)?;
407407+ Ok(SecretKey::from_bytes(&signing_key))
392408}
393409394410#[cfg(test)]
···777793 fn test_list_peers_with_more_than_0_peer() {
778794 let conn = setup_db_schema();
779795 let _local_user = create_user(&conn, ACCOUNT::LOCAL);
780780- save_self_account_db(&conn, "varathan", "did:jey:varathan").unwrap();
796796+ save_peer_account_db(&conn, "did:jey:varathan", "varathan").unwrap();
781797 let user_list = get_peer_list(&conn).unwrap();
782798783799 assert!(!user_list.is_empty())
···787803 fn test_unlink_valid_peer() {
788804 let conn = setup_db_schema();
789805 let _local_user = create_user(&conn, ACCOUNT::LOCAL);
790790- save_self_account_db(&conn, "did:jey:varathan", "varathan").unwrap();
806806+ save_peer_account_db(&conn, "did:jey:varathan", "varathan").unwrap();
791807 let user_list = get_peer_list(&conn).unwrap();
792808793809 assert!(!user_list.is_empty());
+16-11
tiles/src/core/network/mod.rs
···1111use anyhow::Result;
1212use futures_util::{StreamExt, TryStreamExt};
1313use iroh::{
1414- Endpoint, EndpointId, NET_REPORT_TIMEOUT, PublicKey, SecretKey,
1414+ Endpoint, EndpointId, NET_REPORT_TIMEOUT, PublicKey,
1515 address_lookup::{self, MdnsAddressLookup, mdns},
1616 endpoint::{BindError, presets},
1717 endpoint_info::UserData,
···2424use iroh_ping::Ping;
2525use iroh_tickets::endpoint::EndpointTicket;
2626use rusqlite::Connection;
2727-use tilekit::accounts::{
2828- get_did_from_public_key, get_random_bytes, get_random_bytes_32, get_secret_key,
2929-};
2727+use tilekit::accounts::{get_did_from_public_key, get_random_bytes, get_random_bytes_32};
3028use tokio::task::spawn_blocking;
3129use uuid::Uuid;
32303331use crate::core::{
3434- accounts::{self, get_current_user, get_user_by_user_id, save_self_account_db},
3232+ accounts::{
3333+ self, get_app_secret_key, get_current_user, get_user_by_user_id, save_peer_account_db,
3434+ },
3535 network::ticket::{EndpointUserData, LinkTicket},
3636 storage::db::{DBTYPE, get_db_conn},
3737};
···289289 }
290290291291 if let Err(err) =
292292- save_self_account_db(&db_conn, &msg.from_did, &msg.from_nickname)
292292+ save_peer_account_db(&db_conn, &msg.from_did, &msg.from_nickname)
293293 {
294294 println!("Failed to add the peer locally due to {:?}", err);
295295···319319 println!("\nLink accepted by {}({})", msg.from_nickname, msg.from_did);
320320321321 if let Err(err) =
322322- save_self_account_db(&db_conn, &msg.from_did, &msg.from_nickname)
322322+ save_peer_account_db(&db_conn, &msg.from_did, &msg.from_nickname)
323323 {
324324 println!("Failed to add the peer locally due to {:?}", err);
325325 return Ok(());
···346346 // tiles keypair in keychain
347347 let usr_data = EndpointUserData::new(&user.user_id, &user.username);
348348 if !cfg!(debug_assertions) {
349349- let signing_key = get_secret_key("tiles", &user.user_id)?;
350350- let secret_key = SecretKey::from_bytes(&signing_key);
349349+ let secret_key = get_app_secret_key(&user.user_id)?;
351350 Endpoint::builder(presets::N0)
352351 .user_data_for_address_lookup(UserData::try_from(usr_data.to_string())?)
353352 .secret_key(secret_key)
···435434}
436435437436// We handle the parsing in this way since ticket can be an encoded `LinkTicket`
438438-// or just a 5 byte hex if linking over mDNS
437437+// or just a 4 byte hex if linking over mDNS
439438fn parse_link_ticket(
440439 ticket: &str,
441440) -> Result<(Option<EndpointId>, String, String, Option<TopicId>)> {
···456455}
457456458457fn is_did_valid(did: &str, pub_key: PublicKey) -> Result<bool> {
459459- Ok(get_did_from_public_key(&pub_key)? != did)
458458+ // on debug mode, we skip the auth check, since we will be testing
459459+ // with random endpoitns but w DID from config atp
460460+ if cfg!(debug_assertions) {
461461+ Ok(true)
462462+ } else {
463463+ Ok(get_did_from_public_key(&pub_key)? == did)
464464+ }
460465}
461466// fn subsribe_mdns_events(mdns_events) {}
462467//TODO: Add tests, can we get some from iroh reference?