-- CeetOS signed release. Generated; do not edit.
-- CEETOS_RELEASE_MANIFEST:e2NoYW5uZWw9InJlbGVhc2UiLHByb2ZpbGU9ImRlc2t0b3AiLHZlcnNpb249IjAuMTYuMyIsc2l6ZT02MTM0NTEsZGlnZXN0PSI5YTEyMjcyNWMwMGVkNmFmZDU5MGFlZjA2YTYzNjM1OThmNWE4ZjgzZmI4NDQ5ZmNiMGNmZGMwYTUyOThiNzE1IixrZXlfaWQ9ImRlc2t0b3AtcmVsZWFzZS0wLjE2LjIiLG5leHRfa2V5X2lkPSJkZXNrdG9wLXJlbGVhc2UtMC4xNi4zIixuZXh0X3B1YmxpYz0iZFJrVnVRK3Q4M2tHWlVUL0FpcHNkV3VDcmNEUUJ1WnlaMWZNK1UzU2c1SDhkOTR1OFNPWkkra0R3b0JkSVgxS0VIWUtqdmVKaFQxbDVmdlg4dTRpclJCcUhpejhTbXBKSUFMRDlsaTRIeklPd2JoVkI1VXNaQ01NdWVoKzBONHFGUkFUVGZQN0gvK2d2TTFIekpkeHIzR3JLSTk0ZWpDNGpMUUxHc1lWdExYYXAyY2ZKUDVtMW00Wi9LS3VZbDc4U2xnNG5vU0NZQWFDak9XMUJHWXgzbUdHL2MwVWZyeDJtQmNqOXdiK2x1T0RrOGtIZ2xkRjNmMFJOZUZiRHR3K25GcGFQL3hEbllEeGNEZVlFYVlkaHBtSnJvVDRBNzAvV3FCbXBnUTJzSTZPelpJeEJDSHpIRm05UkJ3dWNYb0tuK0N0ZUVHVy9mQlp1NDh0YVlvVmpBT3pWQUV2dEZId2VrbW5SY2o0SDBmQ2thbVJ5bnRpN3N2RGJwSE15b1lrcUxUQ2hjZXZqK0dmRzFyYnJHSW56K2xZTmtWVGpkWUZtVjlSYkh2bVh2ZFdad3gweXkzL2RGU0RyNjZsZ1lEZWdadisrMnI2RGRpNW5YL3o1WjRidWN6QjhjM2wxUHF1OUlnbnFCSThuZjNac1JJRFlRR2d6OFFLL2RsQ3V1bEhzOTJmZGJoWXE3Zk1hc2R1ajl2RVRzK0JSdUtDV25OQ0JJMExtTTIxcTM5S0VCcDRiN0VBUkpvb1gzMmx0ZHFIWG80bVI4T3NVbjdHR1Juem5IMFVQUWhkSWRZK1gxc0VUd1NScHJBS2tPU1dPaERsRC9QOWNSYXIyWHY0QmoxWGJRbFVLaUJVOWVaZ3FzZEU1Z0ZqRTByZ09oQ3F4THc4UTRPYVdQbVJiNjkvTU9Ea1A0anBkL0xWT1lZMlJMck90cmJYdVcycmhFeEpxeGtQYUZsWWNEL3pxVUduc3Y3aC83d1dIVnN2YWNJeDIrUGxJV3l6YmJXcGZoV01XV25XMldFem5ERjk1UlB2QjhVV1RUK3F2aU5PKzhIRFdVeW1BMnlQRUd0cDlvdUtFS2V3U3FXMGVES0FxU0pqSmJucnZsNXpJS3ZadS9XaFp4bE9PQUJRRGdiWWNkQzVSNVNVd2UrSHNmSXh2bWFXbUs3MkFkN3B3NjZTNVYwZExmZGd5NmZWOUdqZk1FSCtOVVZNOW5DZTFqSVNpS1A4QmZMYU5pK2M4R3ByNkUwbmMvaDBGakVJbG5qbXNONzRjdDcxR1FXdmFCQmVoeWkwbGNtbW0zNEFJOU5KNkdGaS9wZ3F0T2RUZHluS2E0VnFRV1pvdlA3Wmwyanhubi9uY2lZOGhkMXhkQnpSeXdEMHFkdllIMUZ1V1dqZWJGSmlJK1pkemRlUStDSkFYM24wZUNzRHdjRUtNcTBtV3FHd1RWd2dFbVIydUpWODl5bE0rSTlFcUdtZldxY0ZDcm9OaURqWTVYZ3pjdVI1N2EwYTh6YUtIUzFUNXJZdEluV2VoSGpTT2Y5Y05yUTFyVmpWTnUvYTFtMGhKdGFvdmYzMm1xK0xiQ1BXVTBCYnFwa3BscEdHY0NlTnhMZ1NnTU12UU43S25TbTRJYmYzaCtMd2lTN1U2eUV2ckdzaFZLeTFMVC9ZZU5jWU5XcDZBQjRQUW81RittOFo5VjYxSjFJQ2ljYkErTXFoQncxSGR3LzVTdjJDeUFqd1RBbmNkYTJGTjNvaXNTZXM2aEY3QUtxczdCKzlreVFwNzI5elBadDlRUFlLYzhpdHZBNkJ2SjQyWmRkOHVZLzRvQ1dVWG9GSHF0T05USytXLzVTMHkzaU5wT0FwSVplZDdXMDhFUmRUb3JuOVdnMUVWN1RmUmNXaUc5blJGbWVHOFR6SHh5RkJNV2dvMGxVQTk0QzBDWkRKOFEyYVNZTVNYTXppLzUwK1M0bGt0RmRKd1BLdXRjdTU3Ulo1MUxvSkZJOVByNWVURGVyeWxFbHRkRGZJbWRUL2VWWm9uSU80NEVZZW5UUUFUSDMyTXZ6TEJyL252SzlFL0FPbi9FVDhqM2pzMnZoV3hpU2I4dDdUQ3lBUmhrRXhVWEpORGZDa0kyN1lqc0hXQ0dSaENqM0pWYSs1QURxNDc2TzJCRml0RHNSV2FBdjRjQ2Z6U3dwZTBMNmw0YnhlZ1RWVk5EbUx1UFAzZnJMRVRuYnc3RkRGRCtmOUtVVk5DVTVsWTc4Y2l2bmZYQko1Nk9oSWJYYkhicTN6dnhCNER5ZmhkSmF0YWNFbTYycFBnYXl2aE1XdWRWYktXQld2Y3VuODR2QmNEdCtSZG54bjZpMUwyR0RLSXJLUnhPRFl1cHVqeWFoM2hOUGh5bVpDdWxrODZZTFRhV01ocmpOckl2Lyt6cmVPRE1Wb1NYaHF3eERKV3lWL0hwaXFhbDc0VXdaaVVWWld3NFlXOHlrZ0FHaVpBWVpZUjVTZHBLdUN2UjV6dDBycDU5Z3BDYVhQeFlVYnZwL0RBNytJMC9MVG5kbzgwb2ZQdHc3ekNRUVR1TXNabGJyUFRoR2E0L0JIUjlqZThlYUNVQmE3Nmx0K1Q2aS8xNHlsUUdnVU1nenMzS1hDNjFHSjNnYktPb0pXYjcyMTUrRUkvN0JIYnpLdS9NVndUNkozNmM0eE02L2lOZFFtRFNjdFVrNWM3dERRWW1yVVZYSEl3eUZ0QnNhVWFCWTMyZ0NFT08vbFBFRnI4dThxWi9obzUzbmQvdnhSeUYrUDhlUU9HZWplRnhtRXNCeW43NWIyQmZRSlRXWnNWY1FNaUE0ME9NZFFtRG54MkhHekRIRDd3cUlZcUpTSEtla2tQaEFMckMxOTJPQi82L05MRm55Ym9YU1FqbmUwK1R3UzhzZmsrVUt0MHlNY0ljcDlBdHRHSmRPbFFxVmZWQ3RCdDEvMTVlZmhUb3hCRHFBd0JsbTVacXJ6NnBKK1luT05TeERpNmoxRnMvZUhPdlhaZWltamRtb1BDS0g5QTdqdmh3K1pkZFM1ZzFGWmp3QzBXbllIMHZZSDJqdUtNWGUvNUorZ05jVkNGcFh4ODl3YUUyUUhsT0pCRnl0dDFLTWY1d1d5OGVZNkV5TkNOb2RaaGdJY1ZrdHcraWNhSnVUSTdvckVpd2pyTVNzaDBObDlSZlUwWE1qTkp4ZkVCQ1ZPNGhBUEpVL2t0eHlGd1Q3MmZwWEE5VmMrL1ZyOU9meFFSWmRZdTJaVDFJd3g4S1FuUDVlc3Jha3lzRjF2SWRnUlRVTC9YTFZOMDRDdjhRc1QyZnVYSWRpK1ZQdExWTm5ONktNdyt1ZnE1TWNSK28vc2hWaVVIY0p3V3dzNVRhRUkzRXh0dGQ3SFEyU05XYkdMY284ZFdLZk9qMXc4S1lEaTl0SndvelRCYkZpaVIwaVRKNkJNZFc3S1pNbWNGdlRQRXM5dThWMGtFdS9SWGZocG5mdGpWZ1hCbmJ5N1d1cnFEWkhsWVpYbHlhSnpZV2pMUDN4WlJlSlJkczFVT2cxU1o5ZjIzbDhBYWR6dXlOWjVIRndvYXVaSDFMdmlNalZ3RVFXald1RlB3bkVPckhzaktYNlZkYm13VE8vNnhjQjFrd0FVS3cxQUJFZlhJS1VSZ0ZWQllOVThUWkFCMWtlMnVQWmdUVTV1alJvY0hQQ1gvNTRJSmd3ZmpxalVSQlZnTnNvUTRxZ0UwMnZhL2d0bHdEdWRXc1piN3RNUlJBbkNSWEFvMjJnaXhvQjVvTDk3VUs2dGxyeWZZcVF6YWZSSzk1RDlXL2Fab2VGZ0ZCUy9jRHZLYXVLTGlKSFFxSG4vRWNETmpXUUJjT2w0a2laNC92M2doNnZ2bWxmUWlkNlE1YXQ1Rng5K3IxKzd3VnNhRUxRVEtreXpabHg4cU9qNEhhb0ovblNKVml1aEJtRTVoUWFxY2NLQ1BaQlZ3Y3FwSjdVSGRXREVEa3haSFNVZVo3YTNNTzRXSk90Mktqa29aL2ZscWFOd3JDNDkrUHgxRTIrcVRKVE9NRDFTeit6Wng2UzA2NmhXNE9nVVVvNjlhWEc5dTlSQ2pnMXpTS3l3aVJPUFprQXdobjNSSWhxUUlxbDl6Q2o5TW1XUFh4d3RMd3Z4dENETDZFUUUzc0g4TjM3MDgwL0FwbFpGejhaakxVOFA2YjB4MkwrRjhWWHRIdFgxakk4eE50SHJ5K1V2VENJeHdlRVBMbTBCcnFMR2pJd2h1MG5MMlptT1lZblVCU3JzU0txMlF1Vm1TU0FhSkw0cWFuWUpVVnAzRHZBelViVTErRTREMGNTTUphTGdFRjRBK3lUQUV5V0JlbEdCZm84eFptWkw5N285Y01VQlg2RUkxTjB6TlNjVUZXTlhJaUN4Nm93bWYrRXZmd2l3OUZaN2EvZDNwVWlPZXhCSFN3UmlyQ3RJS1k4dndYNTJKNVFXOVFUd25IaTk2V3lrbVZybFZSU2FaS1FBU0RXc2ZaSTNENVQwWGhZWUhkS1VBTC9VNzFEVmdyVzN2eXVOVjd1MHdZNTcyUlpMM3VUWDZsRkV0OTQwSUhLbXB4dXJxQ1RRN29nWTdGSFErV213aXpiK0NDZkYyMTZIdTJzeTVIZTRvaU95eHdZNFZ6d0NibGcyU2RpVVFwbEdkcnZ1YVdrR2JyamdHYkMvV3ZkMWNiYWZ5bFFna0NqaVZQZTFmL0E2TkFvbW12VlEyQmFTU29uMHIvRmwrV2dPek95SUJJYUU4SlZTK3lab2QrZW51cTMydWcyNWxaYWExYVpjQ29qbi8zdXRSaGdKcmJtaGZic2VCeWxDQk11cklsby9YTTZ0V0ZDTVg2YlY0MUk5Rms5ME5mdzdVLzBRU0ZIMVdmYTVzOGxBMDZNUkVZdkgwanZzQXNpaWgrMWVDTWRoUTFaK1QwN1lvWEhlWTRzczFvUWFaMUJXZzdXdEFGM0g5QjBrdjBneUQ1NEVEUHYvSGpUU1FPaUhyS1RYSXU3Q3FhcXVpeUZqKzZwaVZEL1VRT2F5TGNEQXIwb0hVZWd6UDhYcjFxdEMyQm1uVENrZUlBNXhlWGQ1a0wxWG1HWHpSK2M5NGwxWUprWnRVczQzU3BYNUp6a0VDbEVESjZvWXg0K3pIOXQ5Wk1WR3ZMaS9iSXpESlZpQlBlemp0Q3hFTndmNkFHK2pLYXg3TktvZWNHSEJxM1NMcFFENGJSbG1vTHNEeGhrUldCTWgwKzJHMGFuQUVYOFROeVJHQTlXWHRqcXlZbS9rakdDeGZ0bXgyT3FpcEFSeS94NlZ4S0JVM3VtUmpRUXllTlRPSFZ2NG5ZeTRLS293ajg5aFBUand4TXd3d1RWaE9NZm9TY1lCR3h3aUh3a1R1K3FlSVhFSDFQcFlCTnZzaXNLcjN4SUN4aVZXSWlHd3E0T2hTV0VveUpBNjdjenRpUitNQ1VzSzArcW4yZS9BR3lWRHhnMU5TZFhzWjJFekhIeDE3TFc1K2dBMDFoc2FVbHB1RHZlb1FScHM4NzdJK0pqRTllcm95dHBFcE5kOTNyMnBIMWFCWGJHTHlwRFdhOVJ1N2lUN2dBNVRPdVdSYmlXNTJLeU1yOXdvWmJFK1B2UHBMWUZMMG12SnVVNzh3Z3k5SXJoZVE1UFRRZGQ3OXRKUEl4WUt5N2RkY0ZMSTlDZWZuc240MlQ1N1ErWFpXemI3MnFFZlA3ditPZUMrVHJYRlZqMStVb0h2TU5SVjlPb0Qwb3R0OGdsbmpka2xJeHRIZFczVDZtTjZQVkVmUVVRS2YzVHhBK2kyUVBCbXljb3hFNkJxNTN2MHExWEVwZENDRjNsOHhMWXVnZERyenZqejBvSmVvcUx1ZWRhTVZ1S092d2pJM2gya1lZUS9qM1F5N0l0TjBVMWhmYUI1amd5Vkk2M2RGRWpnUitaYm9zam5jQjg2ZWI0Rmk2T25JVWdiZWFNQVlCNXk3L3MwMW5rRUNBdXI3S1hzM0pFOTRPdVE5Q2ZSb2ViOVYzQTVudmRabVpwZlZXQXYzb0YyMGp5aWhxa08xYktqOU9NdzM2QTQrQ0VmMjdqUTk5aFB6UEF5ajFXQTVKNXphcEViS0RPRWVYTzN1VmxyUUM4YjhNemhzNkxMSXhxWnBZYnB1NlQ2WEZ4NnFnUmw2SEs0cGJBTThLY0hGQ3ZwL2RLK1ZESnVUdjJVNE1EOVBGV0J4YTNhRVZDc2laM2JmUUs4c3A0cmZDQSt3dWRZTGVoQ3QxeGxjNG5weHFHaHFKTmFpNWk5dDE5RFQ2ZVoyeXRDdlZWaVJPYStFSmhSSEh2NE5XbTBjTkczczgydUVWQTJBVU11TExMMGdLOUhkd0lUakdQeFpCK0pzUFJLTVdJYkFkOXdyZ1oxSUYrbDkyeFVIcVdBMTc1VDk3N3dibGtYK29WL1hpR0RDcVJhd2NEeEVGVkQzN3loQi8xcFhRQWdHN24vNStOR1Btb1Z0RGY0UGJsdUpCSXRrZUJXQmhOSEh3T1ZoY203ajhZZzU4WTl1VFlQS1oyYzZrZGlrR2wzNzV5SlF5RjZleGRiVHVxZkFnZVBDNnpueHMvQ1ZEUFFWQVdXdk45ZEMwZUdPUHZaNUtkRThGQ0RTQXAxcW9FZzluYmtmRElyT2hHNkpwanB3SXYza3o5N1BubUpacXkxSU9vaEhlZ3k0djNHR25RRGZlczV5KzZjMUtzdmVKcjFFaUhBRktlUE1LY2wyWUZCS0EySVhaT3c5QXBOSHhXK2h6WU1GbUVyR052anlVMTN2ZllJNzNLOXFCblNQandDWUxIVWNXcHl4RitkdzVjRldSTjhITFZkUzUwODM0b3Z0QmFXNHpoQ1FZVGRKeDN2dnM1US9GWHl6Um9KOTZNM045am1Oc1pmdzZ5M0kwYkZwOW16b2tJNHI1UHFqcDFkZ29CSWpidHZZUStGTStkYkhITjkwT2dQcHBmN0NuK2t2LzdXbkk4MTVKSGt6aytFVUgwQTNMSEp5MnBUcW5hbk9sRFJKOEFpUUNOSjJ3aUFiSWpHTXpRM2VuaElXaHZVMnZoaHIrOEc3MFBVUGpCbFY3M01RTVdWNGlqTlBDMkl4bTh5TXhQVXJWTGRDWk5HY1J0QVZEbEt6em51RTUxemlOTFZZTmdlRGZKQW1NSWRoeGduZlFCVmFJOW1hN0Q4K0RwMTNSRVFSL1RaaXZhOU9UTEtuY21RVnp5R3FXZ1NDVFJmWHhGV21nM08yYWY3QjhQNURlejJIV0UwaXJlRkQ4OFMrdzk2NksrZEIzRG9DVzg1Q2ZOZnhxRVZCMUZML1NXMXhuTkZsWmtkdFcwU3A3a0ZxMlpzWTF2S1NqMHpDZGdMd0M2VHc0MnVtWWttOWZyUC8wZ0phZXNkK2JSNDQwRkdxVjc5amFwUm1sc2xjMFlKRXBsZm9uWWxtQUU4cWFubStWbXFPVTBFSHErVUVyL0piSnpYd0dYa1g5ck9sNk9uYzlqZ3FaVkZiZ3BlWDJoRDRqbHJtb2pWSEY0Q2ZpN0NLWjRSUDdMMVl5bm81V1p5blVFSTJwTFBtTGVKZkVPa0Y1NHI2RVdyRDBUeE56TUtjckpZR2w2cUdpUTBmT09GdHE5SWx1VnRWK2poNEJKdk10NnJrKzgyYlVLWmxUU0lIUTNUSllrTVRGdGJUMnh3Wm1iM0I3dHNzc0JOTWtlUG1JT21WMXg2bUlBLzY4QktIWEJvejZSUXNYU0hNRUw0K3M1WHA1RjZObFhmWjM2T3NzUTRKUlBOcE0xZUQ4SHdEaWt5cEU4R1NYdmZCd25zLzUxMmtDOWhzUVIwRkQydTJJWXNwR2F4SnV4WG9aZGxJMExRTVZMamI0Z3hJcFdVa0dQV1U5aUg0dnhFY3AzMFlYdXgxdk03R2wrRkVxY2FKVCtiNmdtZldYcjFrWFI0Wkt2QjlpbE1hNk96czM4MHZEYnBscmVneTRuaEZPUEh0L1VSTzEwZXdVY1dDeWRJdXFZaU94Y3RuMzB1R0sybE95SmhXN0dIdzQ1Wi85U2g2S3FkdXBNcGJmRjBDYjZ3TG1FTDFHbnRzd0MvT1VjSC9RdUhLaWhlOGVxZDJUU3YzQ04vbU1POXg0ZFBlaVBrbytzZWRUcGg3eWhMYW9qSXVuRmw2eFoyMFJpSGlaRGtDdG5haENWQ2NOMEZncEt1c0ZuTDIzTXRHWDZ4cTh2UHZEUDlFMGZkWlRoWXJ0YTlYVGE4TzJCbnQxaUhCSUIycnkwVTRqRkdaV0Y0bXNEeWgxR3NINDdwQ2FESzZBVGFaMXV1K2pNRXd3Z2FPSlFUM2pEd0UwL09yd2Z1QUFtNHVZUGtZQ2FEN0xldkpscXErcFYzQURwTUhjbTJjc3hFcFdoWW5FVzc0dUdRSzRjS0psWHQ5K1g2bmJFc2JZSjZwS0hPanpwa2hWUmtEYW5DbWp5VE1TLyswMEo4UFBoUEV0U1ZGN2hrNVVZTjk2SlFRU3lTdnhsNmp3S1lHTXk5MjY3YUI4ZHRYSTVhSVVwbVZRb3BMWFBGdXQ2YmRSMllTUWtlVUNOeWc1ckRHV01QOExIMFpNaGp3MXpGUWx6cjlUNHJ6Sm8zaWFTSnJEOGpjczUwUFkybGRnSytEMXZqcWk3cjhBY0d6VFc4Zzhpc1UzMThac01HM0c0RTEydzk3R1poeTVVb1Bycm81YVNlSkliMVdhZlRibFBTSXhjZGxJMlYxRlp4Qm4zZ253RC9lR2lpa2EvaGpybUd0cFpYQkZxM00vV0xzbDZwV05jN0hWZW9TSlRkaDNKcjM0NS82QTlibGFhSU5oTFg0Vktjbk1UdUdNaXh5U1pMZzNDelhTVXFXRXM5d2xKT1JmOENmVlVPakw1MHJTQnRFQVR2T2ZRMlVqTmZRNGxhSG1WYW1kTWhBSkNvRXNQcnNKNE12MzV0ekxTRW82ZHJlUzdibC9WN3JsUWZnaGtkMU1MekdkMlkyUlNzaFRFTVNvQlVVb2dUZmlPcCt0WnNycFBEb1g3TVd5YzRrRkpaYVdzb0F0QnZ0ZFc3cEZ5S0JONXp5Z3JUT0xxR1BpKzUrL3c0WDFjZlVoekJSenR6azNPOTNWOHJlYTNvUGlNOTZRMmhadlBTUWZZRzhGVWRhRDJ4SlBkY3Q1TUpPbjB2eEd6MDFyT0JrQTFFRjFxMFFBY0g0SkZ5dEozU2lJUTlKeVV3aUZBZ3RQcGdSYUR6SmhXVlhmQ0xwVVJDV1lUM2RmOGEwZ1Y5WFdMZWhueThSNWtTb3RWQzk4bTY5bk5BWnF2WHAwc0Joc0tweG1jemtXNGs4bnBtRmpsdy9mUEdhTmhuZHlIYXFXL1F2b2x0M2w2SzZiaCtzOGZ3RjhTTHpxbVZ4MThVTEpvSTBRcGlWV2dBMjJJM0drbFgrcmM0ZHRMZndQQXdVMmplZ00vVDRjc0lueis0eUVoT29qMWFwMGwxUzZudGttdXpmcUQwQlIveEc1WXdrZVVybWZXWEV5d0pFZWZaelZ5Qk8xYnJ6WDhYRUErRjl2ZERNalNxeEh3dXRPMDZsZTNpMXlEaDRocUFmZ2UxYmdFcjFTSis1TTFKNmNDbnVQVUZJbjRkc0tzL1Rna3JkTkJQald5Um42eWRHdUVZcFhHNzBhVVF2d3dpQVFKR3V0cUcrMVRIRzVXWnh3Rk80YWZmb3YwN0RxUUFxSW9jclVQdFlqRkVIQUMxRnpwbXlsVGl2akg4dnV4NGdQZ0h1Sm9zWm1QYmNnODFlQkh4dEVmVXM3Rk5RcTZXUHBjZlhERm96N2UzNmt0bWRZbi91U3lCVlZaUVRneGhQOHV6VWJJNWdSbHZuejVlUTBFOEpSSitXUDQyREsvM1pVWWpCNGZrSGg0Ukg4dHlXanRyYTdvK2c3c1VTVHFqdkY1L0FiRjYxYisyaWtFcHZCTndOUncxdURVTTRvZnVzK05WbHd3YnYxZjdmcWdHRlBsaG1XY0pGQzBkSHRMTitWd1BRdkN1YVZIMWFBVCtoanhtU3h1K2JYY1hTa1ZSUXZ3NXdQa0F5d0YzdjJVdUlBYzl5cERScDNJWnFScnFmODExdUxkRlE0N21BeXYrYjlveEtFY2JkV0FMcGVkK3BkOGtqeWNZeHJQQU8zU1Z3czc1YXBSYUpkNWlOcGMvQVBnSTRoSmJWbEoxQnFqTmtldWh0Qm51b1BzeU5uUXF6YUJuWlgxTmxWRmt6aktmOWZVdXNxckVhdkJiV0Q5V3hoMXdnMk14Ulk5MjBXMm9lRDNXSjR3MWtodkhVc3hYaDFxUys3bUY0TGswb1hSb1UwVktlcnp2Um9nczlDalpObFBNQlptVUxMcnZkNGNzZndDcGwxQ0NnYXhhbFptVTBpZjNRTkRJRk01d21raVVWc1grQ1ZVZEpWcEJnMlp6MDBtdnJUbUFVWnZrenh6T25sREd2ODZHOUlka0VHclFFMlFiZFp0M0FyR1NLMXAyMnJBN1pCYlIxUXRTK1RUbnFMTlVvM1VLWjRmK0lWOUV4UXI3ekNLdXdNRmJzK0d5aTg0ZDJYaC8xWEhzQ3hUcHpjTzZiQWJJVGdodWZDSFFmVEFZN3laaCtnN0l4L1p3ZzR3VHFQQjdjMU9vQTNpL045ODZzRWJtaDZFcnNlU3AvTThaSmlLbDhhQ2VOdy9HWWdnVjZseVk2NFZuOTU4RzYyT1VuQ2Rxak1sb0Z5VkY2T3hsdDJyZzU1K2swdjdWVlY5QXpVSGtuY0s0c2I2akRZWG1QUmRBTkt4UEkyNERXRzVzczlEVnBkQzBTUTdZSEI5UjQyVUtOMGtRNDBXVWRnS3VkWGE5VzB5QUE3UVRVaGJvM1NkTlRhNXhvWkRSb1YxVlBxUWNGSWZ5dkp5SVdXKzF4WGVYdENTMFg4WnJuRlMwMVQxVHNzdVdkYjhHczRCTVgxelVvUGlpM1VNNW9IMDVZL2lhb0FaZlIxbkhNWmh1b3YxdHp0bFM4Tm5DZ0w5L0h1Zk1uZkx6ZDBxbnJCYkNobDFGY2hJSmhGSU1ScXExU1FJRDhtWG0vY0pibkJXNzYxaXFWaTZ0VytMSG9BNlRRdXgwUHQ2b2J1dTljWnlCanIrL20rL2pJVnpWamxRS0xnY2R2L3pMZVN0ZXVYbUlFbURnRnN0SlRNWVFvaGNBaUF6UmtYSlkyazIzTEpBa0dxeW81SEFXemoyUTlSTXZ5MTQ4T0JEZDJrYm9HS05zcE9sSE01K3RWd0RIYzlWZnI1NWtlK1pJbUlIYmxtMlBMdG5SSU5zdkU3TGdBcWZzNGpsd2xNaXlnTThGa213VGVXOEswYm0yNnQvVGpMUlFId3gweFJRS0Z1Z3o1emJtRnBpOS82Z3JJWWpRVGw5ZDYvdzc3c1VIQ3A2MmJORjVQNFRncVFBejlpN0lWVVBnUERmdzM4REZubDA1QWFqcmNMMU1ocDQ2cHZLMWxRYWdlL0FuYkZ2Z1dyM3VTbDVBYXcxUWFGaG9FeFFiZjQrK2lLUkpEL1Z5b3MyOU8yem0zMXNmK1RiQ2N2UlV0UUNOQ3lYOUtRQWpkODBoODE5NncybmMxRC9HaGRIU21qaUZmOTN5WkllZmZaUTFmMUo5cWN4b1pvaUlCbEZOejdta20veUk2djlVRGJFS3Zzbm5uZzBHejkzVUpMeTEyb05nSHZYMXc0R2pQczUxZzhoUVpQcGJZc0p5MEQvb2l2bGk2L1FsOEdUVnBpS0NoMHdLdWd6UU9INm13LzgvL0kxR2FscW1nSTNIQnZCVTZUWVUrWnNjcjZuNVlBL1BMbEVGNGh4YURCbFd6ZGJrK0hvei9PcisvZmxnTVhpa2dmZzV5OUFXOWt3VnpEeEpyWjV6Q2JINDVVa0hqTXgzNTJ2YnZOc0xnc2JMd2gxTVhyRktrMjNpM3lPeVgzVGFqRXROaTdUZFpWYVF4U3NXbTQ4Y2dZSWxmT2NHY21GTXU1akFIdTVFaHNyaUI4WXJHOTRiWkdLM0xWcG84QzBQMWV5dlFLUzV0TGlIa3lRMUtJMWpRMUF0MHg0OXorWGIxVVN6UUNhaXVtVjR3WFhMWHpMTSs4UzFpL29YdWRRRVVHa1Z3OFcxWkorY1p6VFUvczU3OTVqd25CVzVpQUFJY0hJaU9CdHJ4K2paSCtzeWxuWS9BZUNwY3hiaDFLN1BGR0VzaWErWlR5K0pLSHJNNHNPeENOVEQ3cDVQRkFpTjkrMWU4RU1hakRCcGVPSnNPV1M3dXhWcCs2cDNjV05FL3NLTjltdjdiY0dFSVY3YWtPTXFGR2ZoZGdybjB0NnhYMDBnRURGSFdHM0hKaWw3Q3c5VEZtRkNCcG53QU43REx3Zm9nUXBBZXFva3VHVkpVVDNVWnB0VzhxdVNCdUVvanBUQzBJcWhCa2ZkUkJ6S1FKVkNNeHFPczNhcnVYU2FUVlNzMHQxWCtKMnVZdjlRTEpZVmo0S1BlbjJJblVuVmlERklLeFFBQUtiNXF1dldNSWttRVU1RUxWQW5Ea1h2emFzdVluekM2VlNDN0V6RWRtV0doaURka1R6TiswODExUGU1dTBkOHRncjhmekRpazNFWnJhbmo4d1pZakJxT1A0Q09aZEZSOFNyUEtUdWpXQ2FnbUFCUnVmTlkvbDlDMHlsaGVQaFk5Rkt1UEN1M2FlNnpOUU5xZnFPT3pVN2Riam9MV3RBc0VSQ3FQYkdkd2RMWnV3c3YxTFRueTBnMGV4Zm5HUG1RbUZBTldOYjVWclY1YnRyRnpjdTJhSjROWEVRNjRySk5vS3k5by82VDdFaFJMWUNmV1JmdnRhNFpQUGtpOUxTZ1NmdHN3TDk3ZUdveGN6SkJzNm4yTnNiczRLdnBsZU91RmZRMEgrQ0laWWQvVWdCVVBOVm9qSXhpWUpLM1F6aTQ3RkFGOUNWa2MxaHBrU2YxSENwRXo3bjVzUnVzQ25jaG5PbEgyZ1QyNEQ3eHp2UUx6YjNmZFRneHJIbEthKzZVQ05EeDVxekNzeU02ZEdkRHNYM1VmQmsxYUZEWmEvY2pvRS9nRTduMTQ0ei8yVVUraTBzMGxNdmNqdk9iQmp0bmdwOGVSUTB2d1M5YWt4REt1VWpXdFNPT0cvSHJDRjhRSUNUVS9IWDNvY2pyRFZwZGpoU05hekFNVU5CbmxxMUNjWkdHbFN0Qk55eTFlazh5WXk0QWJTYWZNczFiZDdBQzhCc2ZHWm5XYkFUSTcrc3puVzl2azVCZm1sYm5VaHlzWGxrWEdRT2l5V2pTQlBBNWNXSWtxbW9NRytKU0dvVFNMVTVOb3U1QnJNbnVnVUdIODNESXBrbVRVOXc4Wmd0TU5uM04yOHNWd2V5Y1lvMjE2aDV0Wmk5MDdNWGlEWVhIVnQ0b0hDQXJJaitPVE1rRFVHOXlKZ1lpWVlJNjRqWm9oYVFSczRpMHZic1pVTDFadG1MZUtJS3hYcGZUb2FBT3RWQmtDNG44Nm0wdk1uVEp4dmd5Qmk4Tll1YXBiakpObDI0QkprNTFwRVA2cVdYRitoK3ZiU2lsNlpMejgwa1pWY1pVby84ZkFtZkdXUjUzMFRjVUJ6WldiViswSEdxYXJIV2ZQZG8xMHk5TWd2QzVkbm4xeDRiVjhNYUpTWVg0STBvVmtYQjM2c3lIQWFDT3FISTdkbWtZSTJYR1NqdkJCb2RJNWlMUUtwUlBHRGRDM3pLNUZpSGsrMjdZY1pwbmFkVHpsUHJMVVp2SmtGcUhFcEN0ZzFGODg3OGlYMFZRbUhtT2FzUTYxUEtOaWdqaVZGZnArNmpHZXIyN3NWTTFzTEg1WkxGN2Rha0xLT3ZtQk9SLy9CK1p0Ri9NNnhsUkJTdGtTTHVDcnlUTmNxNVNZRzBnc3hSc3U4QmdlcEt0emNKSVNWWkhscGgyQTBNZEVQMGwrV3FFcGlTUG1ySlovVEJaSUlkdHdiZSthc0h2NkdpM01xQk9DRUdlVDFqRWtrVjhKazBHWUtLOGtYQ0prV1gxUUxlSHhSTmRHUlNzdGxnSyt1aU9JdXZBMm1nS05LNjlVSm1KVGxVb05rYlZIUnRLbkx3MFBiVHdLck03ZGVlTUhickJZNDAxY1ErYzhuVmhwcEx1emJyV2ZSNThsaHBKWHA1SW5YSzhXWk05YVJlZWZWcytGZ1RiOW5CV2RiZ1VBbjY4VFpvWFBMeDgvTlB4VnBFS2dFQnl5cWV2WVNXejRhY21ySVY2VWljTnBhcUZnajF2UkIxZDJGZFpwd0xkYjRrR1R6Ujg4YzlMa2NxRDdUcG1HUTVpblZWbkptZ2tKMUd0S2pYKzB4ZkRMcGpDMGhJYmd0Q1Z3dmxtU2VFd2x3bENwems0dVcxVkF3dlV4cEdjSVdRMUhiQUQ4ZTlwNi9mY2l2TGVtbGVKZkRlMC9mWXhEVWkzcE45cjRNMjU1NDU3bHMxUFk5cFZWNkN0UHowSTl2d2E2aWU0OEJnVnV6YkEvVjRJQ1hBUURsdUpkRXVrcGVnYWlyaGk4WDlKRHhOZjF2N2I4ZHI5eWcySmdkdVBLbXA5clF6Y016ZEFJTnEzWWNHQ1IrSldFQ2kveHZiYjFnc2EzSUVuQU9QcnZGeTJIaFNSa3FTWmgzcXhMYmtxODhQZ2Z6TG9icmtnUXNlQk9hYU1qeFQvQzhGeGsxYnA3OERhS201WWZlcUV4UTExZThhOUVGK3ZLMGtFVWh5UENCTm5LNVA1TXFuRUxaSTA0K3l2dWR3NmkyRkdneWk1b0ROa2hXL2MwMVJRTE1pOU5MMW14Y0padzNSYmI2VGtMeUUza29JVy9qcC9RZDZGdEpvWDdpYk9MYm1VL0tHamUzUlRVYXFVTGdlb0lIdWpUSTFOcUZTNW1WajZlUzlKeTVSUkFzZDMxQkNaOCtnUm1wK0xQZU1vRUUyaDNuTzhWK2ZLOUFJejI5MUpDV1BiM2ZUbUVTcm1VVXJkYXI4YnZCUHliS09ZQjhONlBwQzhLV0I2QklteTE4RzI1SWdRN2piRnhid2dUTXZGb3lncHJDd3c3QmVUc2VreGU3NmZxR2FNQWI2YVdjVm5ldERFVVB4NVpoWWdrMmo2a2J4WnUySzV6TXBkNUVVODZxeEsraE9MZWE4SXlMcFNDVXUxTDZVZ2VBZlN5akpWKzBKUmNkeUlYWE9mclU4dzNrM2Q4endaMHNIS1lMeXlTMHBDRkxsbzZVTnFYbGNHN2I4ZHkrdWlKdzVjWWw3NTQ5STcwY1ZXMmo5bW9TM3RSL3RJbTBmc0pEalAzdy9MZVhVV09kMmJQcU5ENzJTOFB6UGpPVHZJOENCMi9yMTZQeGJ5V25oZ0lzRUVlaENYdXkxcHpCR3hVVDgyd2xSUVVHam5zMnI1OGhBSng5bGphdWMvK2NlVHlySE4vcUJXb0R0eHRVSWlNb2JhUmdQdTdOKzRsdVBXV2ZoS095Qzh3cWJJdzZ1YnN5QmtjVWVPUndwRVhDMSt2VSsySVYrUWloMHdDZisvMkE2Y2IrbDMxRklIQWdLdXpCeGdXc0FBYi9JWWNkVE85MTVydXpRM2k4U3lHSWpmeXdrUXBKZmhwRS81TVo5RG5ocURiUW5XUVBxdmk0VG9QREc3ZTJ0Tlc1ckxIUEhBOE1uUDM1Tk9WN0h6MTYybUtkWmdXa0JqcTBJOEJ4YkptMU9kYWdBU3BXT3JjVVBVY1A3d0VaVHJwN0FZMEJreWR1dCt5U2FxcTg1Rjg4TlMyZXdwWmpvK2xBUW9OR1l4anpVTnc3WlRQcjU5eFlpRjdHMjhSaE5EeTBwM3JvN2RENkJRU1JXSjdrUDQzUm5lVElkVTY3RCsyRFE3WGI4SHVsa2VKbmhsOUZBZEM2YWcxbjlDa2lCbS95a2xOWE5GdW1GVlE4eGFpY0h2cGlvYkkvaFlIN3M1Y1Q1MU94QnRSUm5RWS9GUDE1aCtEeU15UmlIZHBRWi9qVk9DU2VIN1QyTGtLdHJXNUxPa09xUGpaMm11QmVMNUpOOGV0elZ2SjhmREUxMGdVK2dyS2pyWjZJaGN5SFplYXdJTW4xV1VyQ3NoZjVCZnJPN3FDMzM1U2ZxTjBOb3pRN2ZWekFtQ09aTVRjanM5SENHZmhaZ0hoUHBiM2ZmMU5kb3hDN2EvUEdJV0RycHdzbEF4ZFpLZjNUUnVXVHp1WkxIa2RNMTV0VWNDY05TM1doY293ckpDbGdoRFBBRU9BekZzME12QTdhaXlnVW84RnNXU3BIZFgxMnY4UVNOanRKY2t1dDZrc2ZUSE5yYUVsVjRGdExleklhbkwzUlM2WGJySUNJRVh2a3pOTXhhQlAzNndHVjBGNURDeE5Gb25VWVRjZmJIMzRtOTR2c01kRnE4UlZONkRRa0N2WDBoSFJYSDZQbnRheldLSG5PemJ0aG9Fbmg3U3pvQmY3b2ZuTytHV3RKTUMyZTlTZnRMTWJoUUtFYUFDcCtMbWs2bWhEeUdyQnRjUkJ2YXU1OW9scTV1SnRCd0hodjVBSURjbkUyeWcrdG5pWU1CSEZNbVF6L2hBUUtmK201UUpiWU5JWnpadEx0L0pFZnlZdWF4Z3lRMFEzSEY5QS9NQzV1UDhGeUh5YkJ1K0ZLaCtVZXNodXRFaC9teVIvTEhTUU5tUGxiMzcrSnBVb1kvTS94Mk9DT1RkRjgzS0I4NTUva1NORC9VZnZseFVlc2hEcGZjaWcyVXl4V0s4anF1VFhvSENKR2NTTkMxbE1ReHJIZURDMmdUemFpeE5mZkxFSGhobjhUc2RCd0ltbm1ENkVWQVhGNlYyUkIxN1NFb1BJNTJadXJidTNOdFA4T0dYMy9IOHNLenhqUWNmbWdtZ3dOZks5M0YydzR1eWR5VGdCTitYQ2RDZ2hFSFpHZ09FWUVDWXBtTk52bUhHT3VtWmZ1ZDM3T2FvaFM3OE9nck5ZT28vV2tEem9HUnRGMENCTEhuK09HT25pNGNoZ2hNYjIyWVg0bTl0cXl4ckxVZkJjbXJXNjBBS0VHaW16bTZ3RFp6NnRkalRGcFpRVlNYU2E0eW1IL0ZHNnp5bXRNTmZCd3N0Q0tuQUkzajNPNlJWbHpybVBzdjVFVkxKaTBGOW13Ti9JS2RsS25RR3BRTW5DNzlJaEJ0NEpFcENIYm10WlhHYTlzZUkvTGVRbndUa1djSUZ2UldXSHJ6dWxrTzJYVUlMZitQVnZvV2RSa2tlaW9zaWVCclg3TXpzOUFDNENTd1pTZVRSTmNQREVHVEJEcklvdW9qKzdTeDkyM1hHTVRGWHFhamRUczRjV3IvUC9HVU5yNzAvUWZIV1hMRmM4YkN0TTA2a3d1eGpMMlhxZVExc0ZYZmZFS04wT1djUnRHdXYvNDl0b253Vk9FcUswZTBneXJvY2RZS3g1VUI4UDlDZTNBNDQzQkdxdFdhZkdDNkdHZ21JQzAzdUV1c3FDcVdxUUVrVFRYL3NuRlJKNnZUZ016ZUJpNDduRFROdDdEMUVSNmhaZXpNTVM3dlI2anRiTzFLMEhOTlhrS21UaU4yQnl4MFdwRWV6VmJjNUNKTndJRXYwS281eWRwQjBBVkNrVER1KzA3K3k4ekVab3kvd3FHQVRhTEk1MnJvK1pjcTExRUoydzNpSWJETjAxY1R6TWF0ZTVLNy9QTUY5RXgraktFcUo0YU1zTmp6dklHODdpSlBRUUdVeVBQOEVYbnFMa0IzUUxYZTV3UStZWnk0eXdYMXZQdERubjFlREF5TXFveWhYakI3MWttZjUxaFBQSFo2OGNGN0tQNDJ2TFRCV1VycEN5MytBUnRFM3dic3E4Y1dQUmlNbmJXa3Y4NWwzNzcxSlkyZkdxOUJ5L2lwOHFhSU9CZFNtMUdJZFJWdDBGV0d2Vk9pUXhSVjMycVZQc1U3Z2loaW1SY0NpeStVZlFhRWM3UzJteG1wTFA0bVRkcXNqNjFPeC9HR2NGMFBjSUZ5T3RBaG5SZGJweitmYTFzY2xsbGpQTGRVUTg0VXhDQWlWOFZoQ1NrUlBpdENoZG56NHJvZHpQRHY0QnZ4VVNRRXpqZmNuV2oxaGVjMWJEdXg3TElLNDdjYXBaUklMK3VVUmt2bzRXdFU1Q2Niek5vV3loZFZUNm9VUmpVOXA1VG91RlFRSVd4R0pYQkZKZ2tzdTJ0NERZQkJES2x1RTVhQjlXTU5QbXBmTm41YjJ1d2YyNEhBS1hIekJhSmhldkllOFl3R1hjWlBNMFMzSlM3SjlQUXdrZlBCMTIzU2ViSyt6a2twYUpHbjZCbEhCMnNmUnlFZGdtQ24vSlFrTiszUU9CamdpS014TmJHRit1ZUYyRnFOOHg5RkJnZnMrbm1BU0loNElsS1VkVFlzR0tYTGpESG5lQmdOU2swMjZmSUtNQ29ORG42UUlkRHREVC9WV3pqRWRTdm9KZXJDaUpKL2RVd1IreFI1SVV1R2JEY3pCd0hJNFVkUG14cElabFgralJiMkUxa0Z3em50NU5BQmRRRFExZWpzT2RmZGJIMVlDUXRpYjR5US9kc3RNMHRvRkNrWVBvR3g4U1JIRzAxYXozd3dIVHc1QzRpdGpMdFUwcmxSTXZPODl6ejZmTmpSUUgzSWVMQ3pvSG56SmhmVGkxdnkzdmZlWkNpRlBCd0p2c3YybTk5VDh4bVZQZTdCcWN3amcwb3lyWHJTQ1dMRXE0M0dYT3RLKzNQN0ZTK2IwZWFJcWdpSlB1YkU2d1I0YWE5eDhYanRTOVdrbUhDbmFTM1MyVmVkVi9ITlRKVW1uZnp2YWxEZ2dNTDluM0NUTURqbDVPMnR5eDVzeEhlL0RxSkY3Tm1uVWdhem0wRlpCU1hGNHBtNkJ2ZVhUUThXdk1ISkxpb3EyeXpQTWY3RExodnNPakZIUEFzcDQzZE5TWWlkaGd3VHZBbW1CV1NPQnpGRVlnOGpzRGcwWGhTYm5GYUFRY3hPWmJvT1pmSkczU0RVdWJma0ZSbVRPcWpUUDFpVER2Zk9neGxZMWpmOFEwNVpXNks3YzJ4ZWNub2xHNUdiSVMrSVJvUU95VithNkkxV2MxRDI5WTg0cU1ZWUVhbmwwcHdOSFhPZWNyQUtQdU5FSGY5bStJRjVvVklvK1BNTG9RSExzY2x5eVBNSXpKaVYzKysvM2NJaWswSC8rWHUzYjJZK2hoNFk3WWtUc3F1ZkxhRUhtVjNpYWlsWFlxZmJDL0M4Z3ZiYytOeC9jSVMxVlhKeEJlV0phNjlhZVpldnY3bTlsL3YrM2VONHZoeE9PeEY2SHI4T3c1cFdWRU10QzVrbHVrOGVDc0h1UDk2VTVvajFibzNrME9WVkdFNmVRZlEyQkFBeHJjS2M5OExIdW5uT20rM1hQM2oxa3FGQzNaUmxSYW5BaUdNcE8yaGJXbitGc2ZicU1EVTV6Wk5QVCtraXlkUFV0S0Y0RE1zdDdTQXlLYjloMWZ3R1lmWlpKMllWOXNlTWNvMVFGRW5LcE4rYS85aU5IUmsvMTBiYTlzYkQ5RFdQR0FoRjZuYWZuMldEd1IvRFNDcmdOVVVyaE4ybk0vWTZ6NWdHOC9FSkFYaUtsdUl2Zk9ISFdLWXJPbkJmdktRUDNEZ0p0TVlwc0dBbERWWm5iVWp5U05WcVE2Z3A2Y1FBcFdHZG9IN1ArajhuSllhRlFnSkVQbTdWMCtPN0ZtUnNOV2lOaGVzTmYwbVBGc3Baa0NTbDZPYXpwMm5NUFhQcVVoclJYdGU3YlFhWlZiT2NsL0JPbXM4ZDg0Y3R6NTZudEkvTHYwYjFtdzNqZ3ZsZ24xa0s1d3RCdTByM1l5dFNGU2oxR255S2ZmUjJmK1Z5aVJZeUhpQTBhSmlCeW03Qmd3R1JLQjAveU0zQnNyRDZZUHp6WHhaSFRQQ21wdjk4MG5vVVZsajVUb1BveUp5czN1NlFpY25pWnN0dGJERmN4Y2VucjJpU3V6WDVBR041eC9FT3ZpQ0hBRWtiRW53U1BDNytOVHlVSEhXd3VSamF4T3dhMHl0d2RySk1OajVyN1laUmh4YVBqNjZXekpWMjA4aXpGRmN6Z1F3Zkdxdi9nLyswcTdydTFrNnVERDMrK2hSL2pUTURyQzZGMzNNRllncWxPcDFXTHB6SSt4VEV1VWc0ZTFkY2lVZmtrZGE1eWx0VVU1VkRCRkFSMjFRZVNJMFEzZS9GeVZ3bGNYUlg5djFBWHV1TDR6ODdkS2FmNTBxdG92L25DWlJkUXlhZFZMTktvRldabXZhK3JscU1iTFdjZkJPUGJDdDlWK1ExQTJzVk5JZG5ySWdxRkE1UVhqTnpaMWdIMVF6Sm1JMjN3UUpXdENzb2FpdnNUWHlUMENtMXFGWXNMcXdpN0pweDBkQzM0YWVpcDdZZUpXdkVKbEJHMjFCalBNWWFFN1QwZXNDYmI3TmhDckVBc2pMNy9BVDdZSk96bW1RaFFsMmlheHNiS0xxbm0rOC84dFZFemtuV2FyRjNsbGExLzNZWDV4SEMwYTRZaGpaYXE3cEZCRnc2QVFiWmt3UHp5UEZMQkVTVC91eXN5NGtWc3p3VmhkK2FlRFlqMktlaDVua3h2STNkQ09yTEEvNHdvNnc5S1BUWU44ZmVodWY1YXZPbTZReldKSU1HWW9DMkJrNkhva21kb1B3Si8veFI5OXNoTHFkbERUWExLc2VTV2tRLyt1dTN5Y1lrTEEwZ3lNTUpnVkkyRHVjbHcya2oxcVBQWXZaQ1lHNmJybDV0ekFEUmJtNzcwTmduVjZsSis2VWxnMUNyRFB5RE5kU3BqWUdBQlU0WkZCNStKaVl4UFhKS2pPTHdOd1ZrMEhaVGVkc085TVNKQ3ZhbStnc3lScktUVkowcGw3VE02aTVtdEQxSU5YZ2JPR0FtV1dab3BlTVdMOUVGU0FQbzREZDFIQUwvdm90SHVrT2dZdngzdUI0clFCblRSMlMrc3FVN1BnYlQxV3hWUS9KOUlJVDF4eENQdytFZ3VSMHFjWEluSFhsZVBXWjdNOEl6SExtVXBVdzhiTTc0cXR2TzRheHMxbWlZeG1ZdEFFVFpVZUFPaUlza2lJU2hnb2xua0dBeldsRXNrU3VxSTdzMDc1R29HcmZtbFQrVkhDejVQZUVnQkRDZ0U5UUdCN2dNZEs0TWtxb3hldkp5SG1yOFJubnNUQms2NEtOTlpqdlRmbVJxclJ3Z3lXNmdDU3lDWEdqTmJ5b05ZamlFdVpzUks4dzhobmlxZ3ZCNGluc0xIV0dZdTh1dzBPdEdmY0pIUElZSTBKaWc4SStIQTJjT3BHRVRHY3hiMzBHdDBMUzhkb2kyVlUvS0tXSDBqZHJyWTBRbUhuNnZPYk9HaVBnczVHMUcvTkdZNzMvQ3laaFgzNXcxTklBdno2QWlORzZLWDNPWTNjMHN3aGV0VnlxT0xkK1lFOStUblNlSSthK1lGSUtXdVhmdXJzV2NJVjFhWitlU3VsVFJxQlRveVVRSS9BdUZWN1M5T3FaSlBvazBsMHdST2dUSWJVMHNSQWpFcm9lNVA5OW1ZK0JhWXBnSDcrN2cwTUtKM21SYUhQSFgvUXFJM2l6MUZsbEh2THd2MGRBLzJxRVlBdmVqMUhUZ3MySkNHbnpFaWRhNXpFRktCWGRJZnNiZGRwQmZ2MWkyNE5nMEdqM2JtNWxvTTRCend2VytJWVZIOGFLZmJqVkthUXkwWnJRazZzbk9XMWR0Q0lXU25iTHVHN1B6Umk2OFFCOElhZ0tMQW8wS2Fsay8zL1dDSUtPeUhiOEVjNnFCVS9oZ3NrTWIxSG01TVFSZ3ZtRHlJbEpiMUEvN0JySkZPSmVwYUVoLy9HRVViLzRZK0MwQ21YYmdnaTluM0pZZTVlZnNZQUx2NGpiTXM2ZnF6YTBWYlV5aHlDODIyOGFKVUZ4QnhQaDRINW5WdTdtV1JobWd1c0dzcWYyT2c0VTJ6WG1vWGl6Wi9XOUlCdEhoTVNXVHVDb2Qvd0w5NkMxZkJWaXB5bGRZTVRycEQrUHNTRStoa1hIRFVqQUt6aTREYUZOVU0zMHVPdlp0QXRJN2psRkhlSnpWeTlQL3E0dEk3MjVDVFBDaG1VYWZLQUVHUlZRZ3B3djd6ZlhvMVMrTVhkbFJReXY2NlM3TGNxYnRweUtBY2xaYzQ2WVc1SnRmVUdpT3MrWHlQQk5ZNGxyMVNRWlMvbUU2eHZkZ0hqYy9zcjc4WUwwOFdHRjdMZDFrenBoa3B6eVRvS3RNdWUrNlhrMTdjZ08zSlQyVzFKT2FxZU1qUVN4S2hrVlQ0dFVlWEthck0wbVVGQ1p0MUp5MXBKRVFxZUIxQlVMd0lMcW9taDZCQkt5VkhiTjZSdHNMMjlzY1drVVFVTm9rUVUrQ1NlakhPVVlWWmZXb042YW0rbHh4ZGNvbUpuU25KUVA2YllmZ2RrNldZQXJTL1ZwVXl0MFpuZTBFWkNTckd5czkxcC9NWm5lbWZtSVYwZlkvcnpCN2dIc0RsU1lGeENtU1hTa2VKNUtlT3UzZHVXMVNBeFowNW1ZUURYRXcwQ0pUWEJJOURwNFVmeWZ3bzJIcTZUdHhsMGFqZFdvM3Q3ZkZXTUxoTkJZckxaanJKZjl1d3NSdXdIeEppZ2s4b21ESUpPSWE3eTZGT0QwejNRaG1MYlBYZjdrdExWN0hvTjNYQUM4QUxQYjIxeVJVYUkrWmh2U1dDb1BwRHA1THAyaDl2V1lrblk5OEg3VFhwRzIyVk9nbzZYWkVNSHdwK2dXVTlYcUh5UndUbTFtQ05YS29GTk41RU9aeTlOZTNPTjBoZENsVE8rUWwwWHNwUlhBN0VwTmJxT09wZGExWG5NZDc0UFNLOEdwVXNNb3phSlFkSEtuQ3BPaU40RndhaUlGMVQxQ1RPbTR5akVNemFORHVDMHdHMmVsYVVaTmY5SkxmNFNRVXRiSzZFaWI3V3dPcUllMWtiTE5uM0hBRnNwN0pPWHdLNTM2Sk5sWDI3VGZlVXUwa2lMQzFWWCtQbGhybG4vS1pqTExNSUlCdXd4OHlZOG5KVWcvd08vSVBMVFJ1R1ZVeGdpQXNzdkxzZXVmUHl4QzFlajk4eUJCUFB4dStjYlJDV0RJT1hsRGc0d0pQYWMyaCtvSjFnVzJNZlBjNFRWZFFJTGxwbE9KL1BlaUtYUnlzRjFML1lGa0dmdStyKzRGdmhYQVBPNTVqamZ0UllaUVVKZGo0ZTBkcGRGT04wTEZPMFhXaXVIMGIxZjZiKzdjNFlxZ0F5NjFOWEE1R3YxSDNCQ0hFTm5FejlWYW1TU09xUXRMVFd2Q2ZHOFRxZDI2SWdRZ2RDUGFEeS92dlZWYnFqaElhOFZsdzBjcHdSTjVDbnFEdVdUek1WSkQ1RFFVQWtueGJYakVPRU1tYWozK09kRE1RT011ajU1UGVmL0pMOGtwaUxsVkVkTEIzemZJOWdXQUc5M1RVZDAxZStwVU1qNksxTWJEVjVEbUorcUZYQ1ViTU1ETUw5K250Q000Rjd0cklyWUNuUGl5YVRtNmd2WlNYb0hOZnY4ZkNTVmxENmdQdk9kQlV1Ny9EOHRzelNKVyttVHQybzBmeGRkL1dsZDlrdmFGWnpLdWNwVkRPNGFFOGd2VDNHRWttSE1CWlNRcXB6aXpIanVZTGdINkZyVGx3K21ZTlZUelFtR3ZlRjJSMHBTVGhWU05sSGdDTG5yM3YraU9IeE1XL3dXK3l6aURMbkFLRlRlU29LYW11cEpIZ0xzdWZTZTlpdVFaUnhvdlRVNk42Q1EzSHlhVDR4WGNaamRkNHF2SGw0M0c0NHpDVG5SVHIrMGN6cGNDeW9DY2Y2NVFhVlZYYUt5cVJXSCtKV09aSHpncTk3SlNManYwYm9RWjBvaElCbjUrSkxQYWlobXlqKy81MjJZbFVha0thZk9FSXVpeS9RYTVjbkNRb1JxSDZ3S3BPTzFhaWcvSFRGM0Z0UzQ3WkN0dmh0dDIxc1lTcGpLZHhlR0M3SzVDOG9vc050YldPWkhhK01ieXVkK3MvL0tSVEpRUWIrUGEzajY1ZFQ0OHVuSnRQS0wrRmtPeDc0N2d6clFaQVVkOGlienNyaXFCRVhyNTlVNXZYT2xEZkFZaGRXVU5sQzhXTFhCUDE4eVUyei9FNk9MelB6TEJEaVQwSXdZZnVSdThhR2J1RnBhMUg4VGJSbWxES3hWeGE5QjBUUmQxM0VxR2p4dmNLVkRTNVZzV3JmMWk3UUNhUFdEOTJNNDh3RmFZNkJCbkY1M3hhNGFORW5ZMm01VDFwMTJVNHNjNXZ5U2RYQ3ZRbUFSWXczQm9acHl4WDBCbWxwSjg0d1R3VlkwVk9ibDcxd01obUh5MVdjNVpzRS96RVFyeUE5aWZIRU5oWXllS1RGa1JPY29xb0h0Y1VqaUwweFhVVWJXWXRIWTRSWjg3Uk1xNllFZXVqMW1vOVhrLzlzZG1jbWlUSFdvZmRuS09Rb0RHNFg4M0NxdTZNbUxnMFhYcGFlcmxIeWNqeDRRNXJQbGNQUW5BVzN3NmkwdWZlUkFxNUZFZmVmMEhnQ1I4R0p5VGkybytla3ZpSncybTZpdEtYcEI0WU9xTDlpT0c4aVNHU1hiREN0aHNiQ1ZiVWs3OXB3MWx3cGY0bkt6UHE4SDBrUzlINWErU0psOWxyODRoc01UOHVrWjVwYVNqVUJMbnphY1c5VGtVblBGbXovaUxaUVJGNW51U2tGL1dObTlkYUlTNnpOdzg1UEpBNTg5Uk15TnVLbEV3SWxzVDJSckVGMDBWelE1Z3pNSEF2cWt3aVlraVhuaUxIZjI3Z0R3THBwMnBzOS9yNzcxZ3lYcEZCajNtVnE2WUd6T1VHa29tOFhwZHhDYnJkbmZXVnFoY3dhYVBpVlNjemNGaDJ6TFVRaFdqdTRVSlBNeGFsanl4UWZ0c0V6TjdDQjhIWWhWRWN4YXc2TzFQREd0Mk93anpXMGhXKys3bjZaOVVqQlJiZnkrbFNDREpmVlk2MzRTaGlRamQxRDhnNHVpMlh4ZDlqcUJZaytQUlVtUitWRlhUWXZ0Q043NUVFSGhkK094aVhNRmk2Vi96c2FzZjkrOWFhVWxsTkFueTIvOWQ5WWVNVkIrU2VrKzhHSnNjc2ZmTkR0VjR0ZXcwY3NEMjY1VEt0RkhMb2syQWdTRlJhVkhFWTJCSzBXNXRtUzArb2MraFovZmRHSHMvclRpUTVVZEJFVDg5NUxjNm0xNFF5bzVBQUltUzZKbkx3cCtuYVNsd054RS9LYXlic3AwNHcrZndkeWFzSEpYY0diSC81UkF1UEZ3bGFJQjlyS0UwT2tXeS9qT1QrdFhEcENaK2wzN2RacVRLelU5bDV5b2JzRi9GMXdUb09sTHdPMUxvVUJzUkkvREwyLzdaKy9SN0NpbnoxSUxmN0tFRDVwUUEzeFd1MzlFRW5hYiszWUdXT0FFTGs1NWE4U0k3aUtETFo1UlRSVmE2NnhhVmtFa3JObkZKemxKb2VTaU1ZemhNS3BBQXZyNW5Nd2ZCYTJiVlErU25vdlB6WjZycnhLcVNxVStIMm0rOTdBWjZpbnVQVThXY1lCdTVZei94RkZDckh6S2FmbHBZNDJZZlZQTVdUYVVzbjQybXFOSHI5V2ZlUzRRT2Z1THVOUTdiLzhtUUd6MS9kREFBZ3hKQVp2aUFMMHBwZGFKanhybHZ4T1dMNmtnUnMwY0p4N1VrZlNKWU44RTAwNmpNZ1U3VnVGVUNPR2FCR1Qvd0k3U3pBMGRLTzhTMG9jamdGemtEMEhBNzNkOVBNUG5UTGU2eGNGM3hrc054S2MrNC9PNXo3bzJBNFVlcGwvbWlqUkorRTBiNjFRVmRBb2YyZXBpaFlWRUltdFVPYWl5VzRlNWRpSjNtRFRZKzNORUtzQmVKMUd5YnZRSzB0OHQvU1kvcmM1cE90dTl3aUNNTEJDWHF3WEVOUmZqR3lXaVFMT2ZxRjV2c0Y4Z05qTGRiSnJKbU54RXp2TzRZUjg4Yk4xbDRVZ0FzSmZNT2FBVm1sK2hYQWZIYk1Peit4NlpKVlNoUkwxZGFUbE9YYXR0RmJMS093OEhrTmU2alRGOEh5a1dCYU1PZFhVMXNsbllIN1lJbkxaV1FLa2xHMEdtb1MxUTBYbFFTdFFiSjRTQUFndlUxN1NOemlKUm5BNVFTZzZOdm9lWVJRM1ZTandVQXAvR0RyaDRDQzdCK2EyOXhaOUN3a0hsb2kxazBGOFEvR3I0QW9JcXZCZmlYNjJoRW1HcVczcFNBWENvZU5Kc1lTR0xvLzhCb1FQV3Jnb0JrY3JZZkVTc2xGdnVUUE8wR3BNeGFuYU1KeXNMNldhOGI0d2tsditWU3pmYlBBTGVhQmI3YkkrYWtiaHhSaVRhZndxeXVhVFlBU3pVVjg5QUN5QThFbnA2aWlhWFd3NTNaOHM3UU1IN0t5d1l0UjBCUEdaT0FEZHFBeENubnRRdkZGOENWQm1uTEo1MGMwZ09vcFhTZ1FnMWhiZThNakFDcm1aTXR1RG1kU1l4RGRRVVpOamhXY05ocXZpZWJqNXIwQUNzdFdKZ3BpbG5McXRKU3pCWFRubXNMRkVCRW1IWnFqZkNHb1dCYWs4ZmhkSDUyZklmWkxCVnZjMXU5K0VpQVlLV3J5YkZCMFd5VklCZHVld0dwYWtZZGEzSjBoWDN0Z2xxWXQ5MXFMZDlKR1Vpd2t0Ukx3WEpPTkhqdmZwdVkwa0g4ZU12K2dCUEZYTkJiVHJFb3Y1RXpORlFORHZzWENZTkQxckdhTDMzT2dXVFdIVUIxKy9INkxWN1htaXFsQjM5azdSY3F1eWV4dm4vdlpXdWQ4NmJocjl1aUVjdnN2REtFV2lTK01ma0pGTlAyUXlGZlpNSVNJQ1RiMDNpTHNFcmwzcHg4WWpmK0ZqaU9GdFRmZkR5VkxOWE5PTTk4WkVDMVRvTmFXTW1CODdacVc4YWJ3NkdMRm03cmtxYXB5TTNSOExMTlcwOTFad09OeFFYN0t6aWZwVTdUYW5MTTlvWVlRQ3FNcDdSTlBNdk9zUGJRY2JzWDNuNUVTMU1MU21FV296aVA3S09GUVZnMStzeGdXa0JxSFVGakhOMDJHdDV5b3o3aEZ5NGY2NWVoK0dBS0VJakxvcXpsNjQ0ejhuNVhKZEdwN1VpUDdpSlJ6bmxmaThuaEdrRnNDWk1EOGpnOXczZDZGc0UvZFhuKy9GcXg2cVEzUTFoQktWYUQ5M2hnb1Rrb1FSVUhxMnhMcnk4c25MbXJhSE1PbUEzWW1CNTk1SXN5dGpPcTJuT1pkWDBVYzM3R0FIUkNXcjZsSVFBenA4eXczUXlQNjgrcGx2OCs4V0RRWi9Zc01rdER5ajNRSU9NZ2RXRHZwdDZDbzBOVjRDd09JOWp3OW4wYTk2dHN4REhvRVIvSk4xZnk4ZkQxangycGwxWEJlZlR4ZUwvZ2t4dFlLaEdIM2R0ZU0vdkY3NkdySDRvU2NSc3lyZ2xoV1Mzd2tTQ2dKRWlyNldLb2p5dFdDWXA3bitiU1pPSTIzaDN0VzN2bXJSdmgxM0RlSzdHbzJuRDdXRjkrdjMyeklybG5jeDBvYVVRUXdmT2I3YUladFY3eXZnVWRqc29mSEQxaTJqM3BmM21PM3ZZY21mTC9kemVsNUE4WkhzRXNkUEhHbzlYS0hoVVU5TjNqRTR1YmhNUEhMU1lPWkxJMWZkeGtVME85ak5EN2xoaC9zVExURmkweXNoaW0xWGQ2VnhCcW5DSGRFL3diZUIwSU9rcEhTUW5HNjQ1OVNNWmRVNHI4VStBMG9JZ3hXMWhiZlRuRFFKcWk2amp3TVZJQWt6K2xPVmxaQUsrUnQwc3p1aW9FSW9uK3NmYUR1eFlza3BEd0lDSm8vWW0vNEhxUGFqU1BqemdKT0FucVdFK0tIc3ZNV1FXWmhIbWJaeklXQ1ZVRXZseEVOK0M2MWlpNStpWm1jWDVUREx4UlBzUGdQMEdtZzEyRjF1NEJEZ21HUVJteE51Uy9lb1luQzVaaUo4aHZlM3pYcEtOWWMyZlNhZ1RIWTVZSWZLRGtKcTlWNUg3N1pyOG1SWnhJWCtiVmxPN1VmVUdqdzdSMmtaRUpEanFqQWtaZWNHdmFLZk9SR0hhVjM3azBKa3FnMUYrTG42UU5zandHNmZWMWJoQXB1eE9ZSkNwTkRXSXViZXFiMlJmTTNsaEh4RXYxNk1GUHR1V1ZaM2x2TnhxcldYbU9ld1huTFdMeEkrUlNlcnNRbnYrcmlaNTRTdTVINkl0V1JEdDBCMFBYYkd0dlhWeERVSmtGd2ZDbkZLQzErbmZ1a3JSeTFTRDZPZUkyN3lCQmIzdDVhWmlYQ3I5L0I3aWNrZTdCVTd6Q0pSaXlleEZ3djg5anVPakRYUWdma255RjQwVkpFVmZVOUp2Wksrd2JKWitRWCs4dUtDdFdmcmM2NE9SWlo4aFY0djBQMkQ3TVRrL3BLdGs3Q1d5ZDlCcFF2UkUzUnZDVDU1WUpYZ1RSY0JFOENzaXFmM0JkUkdTLzQ4OXVucnRrZG9tRHgvNmM2RURDZ25YQmNCbDdScStXeEpFMExJUThMR3JOTENoK05uaDM1bzQ5Q1JDdmEzeDF4b08rYWw1Mm1QSEJwQVZSMmhwdEJMajZDMktvN1U3amZvYkdDcjU0USs3WDIrRkova1NVeXFVRUo3RXg0bVdxNnRVcFluSU9CVGdpbE82OWhoV3U5VGc5TTNsQzMzOWM0a2JyWDVVMG5PUXFoNEtlZkc4aDkvOWxuYXJsbHprU29QYVJsN1VxYXpDL2oxR2swRXVoSGljYlA4eWN2cnFXOUVKUTFoQW9TUGw1empURmFmR2pLZW1iclNFcWlRTzgrK21WU0k3ZDlpVjJRa3RMQTZwT2lSNFp4bElWTVB0RVV5dGVVeGFQRDlSaDR3WDdxYlVYRmYrQW9PSUlsaWxIbHRZam5JblVuVDdHcFF1Z056VzRxNWptSy9zbU54UGVxNVA5bFBJdVVFWEMvbnR3PT0ifQ==
-- CEETOS_RELEASE_SIGNATURE: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
-- CEETOS_RELEASE_PAYLOAD_BEGIN
-- CeetOS installer. Generated; do not edit.
local files = {
  ["startup.lua"] = "local pendingPath,releasePath=\"/ceetos-updates/pending.lua\",\"/ceetos-updates/release.lua\"local pending if fs.exists(pendingPath)then local loader=loadfile(pendingPath)if loader then local ok,value=pcall(loader)if ok and type(value)==\"table\"then pending=value end end end if pending and pending.state==\"apply\"and fs.exists(releasePath)then shell.run(releasePath,\"--ceetos-apply\")elseif fs.exists(\"/ceetos/startup.lua\")then shell.run(\"/ceetos/startup.lua\")else shell.run(\"shell\")end",
  ["ceetos/startup.lua"] = "local root=\"/ceetos\"local profile={id=\"desktop\"}if fs.exists(root..\"/profile.lua\")then local loader=loadfile(root..\"/profile.lua\")if loader then local ok,value=pcall(loader);if ok and type(value)==\"table\"then profile=value end end end if profile.id~=\"desktop\"and fs.exists(root..\"/server.lua\")then shell.run(root..\"/server.lua\")elseif fs.exists(root..\"/ceet.lua\")then shell.run(root..\"/ceet.lua\")else printError(\"CeetOS is incomplete; starting CraftOS shell.\")shell.run(\"shell\")end",
  ["ceetos/profile.lua"] = "return {schema=1,id=\"desktop\"}",
  ["ceetos/ceet.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local net=require(\"ceetos.lib.net\")local peripherals=require(\"ceetos.lib.peripherals\")local telemetry=require(\"ceetos.lib.telemetry\")local ownerService=require(\"ceetos.lib.owner_service\")local audit=require(\"ceetos.lib.audit\")local store=require(\"ceetos.lib.store\")local jobs=require(\"ceetos.lib.jobs\")local crafting=require(\"ceetos.lib.crafting\")local craftSources=require(\"ceetos.lib.craft_sources\")local networkIpc=require(\"ceetos.lib.network_ipc\")local updater=require(\"ceetos.lib.updater\")local profile=require(\"ceetos.lib.profile\")local cloudSync=require(\"ceetos.lib.cloud_sync\")local okVersion,version=pcall(require,\"ceetos.lib.version\")version=okVersion and version or{string=\"0.16.3\"}local SHELL_REPLY_PATH=\"/ceetos/data/shell-replies.lua\"local CRAFT_REPLY_PATH=\"/ceetos/data/craft-replies.lua\"local DEV_NETWORK_STATUS=\"/ceetos/data/network-status.lua\"local DEV_NETWORK_HEALTH=\"/ceetos/data/network-service.lua\"local DEV_NETWORK_IPC_DIAG=\"/ceetos/data/network-ipc-status.lua\"local JOB_INBOX=\"/ceetos/data/job-inbox.lua\"local JOB_OUTBOX=\"/ceetos/data/job-outbox.lua\"local CRAFT_STATE=\"/ceetos/data/crafting-state.lua\"local CRAFT_COMMANDS=\"/ceetos/data/crafting-commands\"local CRAFT_COMMAND_LEGACY=\"/ceetos/data/crafting-commands.lua\"local CRAFT_RECIPES=\"/ceetos/data/recipes.lua\"local PROFILE_CACHE=\"/ceetos/data/profile-cache.lua\"local lastJobTemplateSync=0 local lastProfileAdvert=0 local craftRecoveryChecked=false local function writeDevValue(path,value)if not fs.exists(\"/ceetos/data\")then fs.makeDir(\"/ceetos/data\")end local encoded,serialised=pcall(textutils.serialise,value)if not encoded then return false,tostring(serialised)end local handle=fs.open(path,\"w\")if not handle then return false,\"could not open diagnostic state file\"end handle.write(serialised);handle.close()return true end local function craftDefault()return{config={ticker=nil,requester=nil,recipeServer=nil,stockName=nil,requesterName=nil,orderAddress=\"Order\",timeout=120},sources={tickers={},requesters={},recipeServers={},updated=0},stock={},stockUpdated=0,cart={},craftMode=false,queue={},retries={},history={},sequence=0,acknowledgements={}}end local function craftState()local value=store.read(CRAFT_STATE,craftDefault())local defaults=craftDefault()for key,fallback in pairs(defaults)do if value[key]==nil then value[key]=fallback end end for key,fallback in pairs(defaults.config)do if value.config[key]==nil then value.config[key]=fallback end end if not value.config.ticker and value.config.stockName then value.config.ticker={transport=\"local\",name=value.config.stockName}end if not value.config.requester and value.config.requesterName then value.config.requester={transport=\"local\",name=value.config.requesterName}end if not craftRecoveryChecked then craftRecoveryChecked=true if value.active then local active,item=value.active,value.active.items and value.active.items[value.active.index]value.history[#value.history+1]={id=active.id,status=\"interrupted-unconfirmed\",name=item and item.name,requested=active.requested or(item and item.count),accepted=active.accepted,dispatched=active.delivered or 0,destination=active.destination,reason=\"CeetOS restarted; request was not replayed\",finished=os.epoch(\"utc\")}while#value.history>24 do table.remove(value.history,1)end value.active,value.nextRefresh,value.recoveryNotice=nil,0,\"An active Create request was interrupted and not replayed.\"store.write(CRAFT_STATE,value)end end return value end local function saveCraft(value)store.write(CRAFT_STATE,value)end local function craftRecipes()if not profile.capability(\"recipes\")then return nil,nil,\"Craft mode requires a reachable Recipe Server\"end local recipeDb=require((\"ceetos.lib.recipe_db\"))local base,databaseOrErr=recipeDb.loadBase()if not base then return nil,nil,databaseOrErr end local database,overlay=databaseOrErr,{recipes={},tags={}}if fs.exists(CRAFT_RECIPES)then local file=fs.open(CRAFT_RECIPES,\"r\");local raw=file and file.readAll()or nil if file then file.close()end local parsed,err=crafting.parseRecipeOverlay(raw or\"\")if not parsed then return nil,nil,err end overlay=parsed end return recipeDb.mergeOverlay(base,overlay.recipes),recipeDb.mergeTags(database.tags,overlay.tags),nil,overlay end local function peerSourceCandidates()local tickers,requesters={},{}for _,peer in ipairs(net.peers())do local cache=store.read(\"/ceetos/data/remote/\"..tostring(peer.id)..\".lua\",{})for _,item in ipairs(cache.peripherals or{})do local source={transport=\"peer\",peer=tostring(peer.id),name=item.name,type=item.type,mode=item.mode,label=\"Peer \"..tostring(peer.label or peer.id)..\" / \"..tostring(item.name)}local kind=tostring(item.type or\"\"):lower()if kind:find(\"stockticker\",1,true)then tickers[#tickers+1]=source elseif kind:find(\"redstonerequester\",1,true)then requesters[#requesters+1]=source end end end return tickers,requesters end local function recordProfile(peerId,body)if type(body)~=\"table\"or type(body.profile)~=\"string\"or not profile.IDS[body.profile]then return false end local cache=store.read(PROFILE_CACHE,{})cache[tostring(peerId)]={profile=body.profile,version=tostring(body.version or\"\"):sub(1,24),seen=os.epoch(\"utc\")}store.write(PROFILE_CACHE,cache)return true end local function refreshCraftSources(state,queryPeers)local tickers,requesters=craftSources.discoverLocal()local recipeServers={}local peerTickers,peerRequesters=peerSourceCandidates()for _,item in ipairs(peerTickers)do tickers[#tickers+1]=item end for _,item in ipairs(peerRequesters)do requesters[#requesters+1]=item end local profiles=store.read(PROFILE_CACHE,{})for _,peer in ipairs(net.peers())do local advertised=profiles[tostring(peer.id)]if advertised and advertised.profile==\"recipe-server\"then recipeServers[#recipeServers+1]={transport=\"peer\",peer=tostring(peer.id),label=\"Recipe Server \"..tostring(peer.label or peer.id)}end end if cloudSync.recipeOnline()then recipeServers[#recipeServers+1]={transport=\"cloud\",label=\"Cloud Recipe Server\"}end table.sort(recipeServers,function(a,b)return tostring(a.label)<tostring(b.label)end)state.sources={tickers=tickers,requesters=requesters,recipeServers=recipeServers,updated=os.epoch(\"utc\")}if queryPeers then for _,peer in ipairs(net.peers())do net.request(peer.id,\"peripheral_list\",{},{safe=true});net.request(peer.id,\"profile_query\",{},{safe=true})end end end local function sourceAvailable(source,needle)source=craftSources.kind(source)if not source then return false end if source.transport==\"peer\"then local tickers,requesters=peerSourceCandidates()local candidates=needle==\"stockticker\"and tickers or requesters for _,candidate in ipairs(candidates)do if craftSources.key(candidate)==craftSources.key(source)then return true end end return false end return peripheral.isPresent(source.name)and tostring(peripheral.getType(source.name)or\"\"):lower():find(needle,1,true)~=nil end local function pushCraftHistory(state,row)state.history[#state.history+1]=row while#state.history>24 do table.remove(state.history,1)end end local function failCraft(state,reason)local active=state.active local message=tostring(reason or\"unspecified crafting queue failure\")state.error=message if active then local item=active.items and active.items[active.index]pushCraftHistory(state,{id=active.id,status=\"failed\",name=item and item.name,requested=item and item.count,accepted=active.accepted,delivered=active.delivered,destination=active.destination,error=message,finished=os.epoch(\"utc\")})end state.active=nil end local function cancelCraft(state)local active=state.active if not active then return false end local item=active.items and active.items[active.index]pushCraftHistory(state,{id=active.id,status=crafting.cancellationStatus(active.issued),name=item and item.name,requested=active.requested or(item and item.count),accepted=active.accepted,delivered=active.delivered or 0,remaining=active.remaining,destination=active.destination,issued=active.issued==true,reason=active.issued and\"Create package may still arrive\"or\"Cancelled before dispatch\",finished=os.epoch(\"utc\"),})state.active=nil return true end local function completeCraftItem(state)local active=state.active;active.index=active.index+1;active.plan,active.step,active.requested,active.accepted,active.delivered,active.remaining,active.deadline,active.issued,active.issuedKind,active.issuedAt,active.requesterConfigured,active.requesterStrict,active.waitingFor,active.expectedOutput,active.craftStep,active.pendingCraftRequests,active.craftBaseline,active.nextCraftDispatch,active.packageIndex,active.packageRequest,active.peerRequest,active.planRequest=nil,1,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil active.packageRemaining,active.stepAddress,active.stepRoute,active.packageCount=nil,nil,nil,nil if active.index>#active.items then state.active=nil else active.phase=\"prepare\"end end local function applyCraftCommand(state,command)local status,detail=\"ignored\",nil if auth.allowed(\"operator\")then if command.action==\"add\"and type(command.name)==\"string\"and tonumber(command.count)and command.count>0 then crafting.addToCart(state.cart,command.name,command.count);status=\"ok\"elseif command.action==\"remove\"then table.remove(state.cart,tonumber(command.index)or 0);status=\"ok\"elseif command.action==\"edit\"and state.cart[tonumber(command.index)or 0]then state.cart[tonumber(command.index)].count=math.max(1,math.floor(tonumber(command.count)or 1));status=\"ok\"elseif command.action==\"mode\"then if command.value==true and not sourceAvailable(state.config.requester,\"redstonerequester\")then detail=\"Craft mode needs an available Redstone Requester source\"else state.craftMode=command.value==true;status=\"ok\"end elseif command.action==\"destination\"and type(command.value)==\"string\"and#command.value>0 and#command.value<=64 then state.config.orderAddress=command.value;status=\"ok\"elseif command.action==\"submit\"and#state.cart>0 and not state.active then state.sequence=state.sequence+1;state.queue[#state.queue+1]={id=\"craft-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(state.sequence),items=state.cart,craftMode=state.craftMode,destination=state.config.orderAddress};state.cart,state.error={},nil;status=\"ok\"elseif command.action==\"retry\"and state.retries[tonumber(command.index)or 0]and not state.active then local retry=table.remove(state.retries,tonumber(command.index));state.queue[#state.queue+1]=retry;status=\"ok\"elseif command.action==\"cancel_active\"and state.active then cancelCraft(state);status=\"ok\"elseif command.action==\"refresh\"then state.nextRefresh,state.nextSourceRefresh=0,0;status=\"ok\"elseif command.action==\"source\"and(command.target==\"requester\"or command.target==\"recipes\")and command.value==false then if command.target==\"recipes\"then state.config.recipeServer=nil else state.config.requester,state.craftMode=nil,false end state.nextRefresh=0;status=\"ok\"elseif command.action==\"source\"and command.target==\"recipes\"then local source=type(command.value)==\"table\"and command.value or nil if source and source.transport==\"peer\"and type(source.peer)==\"string\"then state.config.recipeServer={transport=\"peer\",peer=source.peer,label=source.label};status=\"ok\"elseif source and source.transport==\"cloud\"then state.config.recipeServer={transport=\"cloud\",label=\"Cloud Recipe Server\"};status=\"ok\"else detail=\"invalid recipe server\"end elseif command.action==\"source\"and(command.target==\"ticker\"or command.target==\"requester\")then local source=craftSources.kind(command.value)if source and type(source.name)==\"string\"then state.config[command.target]=source;state.nextRefresh=0;status=\"ok\"else detail=\"invalid source\"end else detail=\"action unavailable\"end else detail=\"permission denied\"end if type(command.token)==\"string\"and#command.token<=80 then state.acknowledgements[command.token]={ok=status==\"ok\",error=detail,at=os.epoch(\"utc\")}end end local function takeCraftReply(requestId)if not requestId then return nil end store.invalidate(CRAFT_REPLY_PATH)local replies=store.read(CRAFT_REPLY_PATH,{})local reply=replies[requestId]if reply then replies[requestId]=nil;store.write(CRAFT_REPLY_PATH,replies)end return reply end local function newCraftRequestId(prefix)return prefix..\":\"..tostring(os.getComputerID())..\":\"..tostring(os.epoch(\"utc\"))..\":\"..tostring(math.random(1000,9999))end local function requestPeerCraft(source,typeName,body)local requestId=newCraftRequestId(typeName)body.request_id=requestId local ok,err=net.request(source.peer,typeName,body)return ok and requestId or nil,err end local function refreshCraftStock(state,source,now)if not source then return false,\"No Stock Ticker source selected\"end if source.transport~=\"peer\"then if not sourceAvailable(source,\"stockticker\")then return false,\"Selected Stock Ticker is unavailable\"end local ticker=peripheral.wrap(source.name)local ok,stock=pcall(ticker.stock,true)if not ok then return false,tostring(stock)end state.stock,state.stockUpdated,state.error=stock,now,nil return true end local pending=state.stockRequest if pending then local reply=takeCraftReply(pending.id)if reply then state.stockRequest=nil if not reply.ok then return false,tostring(reply.result or\"peer Stock Ticker request failed\")end state.stock,state.stockUpdated,state.error=reply.result or{},now,nil return true end if now-pending.sent>5000 then state.stockRequest=nil;return false,\"Peer Stock Ticker did not respond\"end return false,nil end local requestId,err=requestPeerCraft(source,\"craft_stock_request\",{name=source.name})if not requestId then return false,err end state.stockRequest={id=requestId,sent=now}return false,nil end local function consumeCraftCommands(state)if auth.refreshSession then auth.refreshSession()end if fs.exists(CRAFT_COMMAND_LEGACY)then store.invalidate(CRAFT_COMMAND_LEGACY)for _,command in ipairs(store.read(CRAFT_COMMAND_LEGACY,{}))do applyCraftCommand(state,command)end store.write(CRAFT_COMMAND_LEGACY,{})end if fs.exists(CRAFT_COMMANDS)and fs.isDir(CRAFT_COMMANDS)then local names=fs.list(CRAFT_COMMANDS);table.sort(names)for _,name in ipairs(names)do local path=fs.combine(CRAFT_COMMANDS,name)local handle=fs.open(path,\"r\");local raw=handle and handle.readAll()or nil if handle then handle.close()end fs.delete(path)local command=raw and textutils.unserialise(raw)or nil if type(command)==\"table\"then applyCraftCommand(state,command)end end end local count=0;for _ in pairs(state.acknowledgements)do count=count+1 end while count>32 do for key in pairs(state.acknowledgements)do state.acknowledgements[key]=nil;count=count-1;break end end end local function tickCraftQueue()local state=craftState();consumeCraftCommands(state)local now=os.epoch(\"utc\")if now>=(state.nextSourceRefresh or 0)then refreshCraftSources(state,true)state.nextSourceRefresh=now+8000 end local tickerSource,requesterSource,recipeServer=craftSources.kind(state.config.ticker),craftSources.kind(state.config.requester),state.config.recipeServer local recipes,recipeTags,recipeError,recipeOverlay=craftRecipes()state.recipes,state.recipeTags,state.recipeOverlay=recipes or{},recipeTags or{},recipeOverlay or{recipes={},tags={}}if recipeError then state.error=recipeError end if not tickerSource then state.error=\"Select a Stock Ticker source\";saveCraft(state);return end if now>=(state.nextRefresh or 0)then local ok,err=refreshCraftStock(state,tickerSource,now)if not ok and err then state.error=err end state.nextRefresh=now+(state.active and 500 or 5000)end if not state.active and#state.queue>0 then state.active=table.remove(state.queue,1);state.active.index,state.active.phase,state.active.step=1,\"prepare\",1 end local active=state.active if active then local item=active.items[active.index]local timeout=(tonumber(state.config.timeout)or 120)*1000 if active.deadline and now>active.deadline and active.phase:find(\"^await_\")then local waiting=active.phase==\"await_crafted_output\"and\"recipe output in Stock Ticker\"or active.phase==\"await_package_confirmation\"and\"Stock Ticker package confirmation\"or\"Stock Ticker delivery confirmation\"failCraft(state,\"timed out waiting for \"..waiting)saveCraft(state)return end if not item then completeCraftItem(state)elseif active.phase==\"prepare\"then if active.craftMode then if recipeServer and recipeServer.transport==\"peer\"then if not active.planRequest then local requestId,err=requestPeerCraft(recipeServer,\"recipe_plan_request\",{item=item.name,quantity=item.count,stock=state.stock})if not requestId then failCraft(state,err)else active.planRequest,active.phase={id=requestId,sent=now,source=recipeServer.peer},\"await_peer_plan\"end saveCraft(state);return end elseif recipeServer and recipeServer.transport==\"cloud\"then local plan,planErr=cloudSync.recipePlan(item.name,item.count,state.stock)if not plan then failCraft(state,planErr or\"cloud Recipe Server is unavailable\")else active.plan,active.step,active.phase=plan,1,\"craft\"end else local plan,planErr=recipes and crafting.plan(recipes,state.stock,item.name,item.count,{forceRoot=true,tags=recipeTags})or nil,recipeError if not plan then failCraft(state,planErr or\"recipe database is unavailable\")else active.plan,active.step,active.phase=plan,1,\"craft\"end end else active.phase=\"deliver\"end elseif active.phase==\"await_peer_plan\"then local pending=active.planRequest;local reply=takeCraftReply(pending and pending.id)if reply then active.planRequest=nil local plan,planErr=reply.ok and crafting.validateExecutionPlan(reply.result)or nil,(reply and reply.result)if not plan then failCraft(state,planErr or\"recipe database did not return a valid plan\")else active.plan,active.step,active.phase=plan,1,\"craft\"end elseif pending and now-(pending.sent or now)>5000 then failCraft(state,\"recipe database did not respond\")end elseif active.phase==\"craft\"then local step=active.plan[active.step]if step then active.stepAddress,active.stepRoute=step.address,(step.dispatch==\"requester\"and\"Requester -> \"or\"Stock Ticker -> \")..tostring(step.address)active.packageCount=step.inputs and#step.inputs or nil end if not step then active.phase=\"deliver\"elseif step.dispatch==\"requester\"and(not requesterSource or not sourceAvailable(requesterSource,\"redstonerequester\"))then failCraft(state,\"Recipe \"..tostring(step.name)..\" needs an available Redstone Requester for Crafter\")elseif step.dispatch==\"package\"then if active.craftStep~=active.step then active.craftStep,active.packageIndex,active.craftBaseline=active.step,1,crafting.normalise(state.stock)[step.name]or 0 end local input=step.inputs and step.inputs[active.packageIndex]if not input then active.phase,active.baseline,active.waitingFor,active.expectedOutput,active.deadline=\"await_crafted_output\",active.craftBaseline,step.name,step.produced,now+timeout state.nextRefresh=0 elseif not active.packageRequest then local baseline=crafting.normalise(state.stock)[input.name]or 0 local requested=active.packageRemaining or input.count local ok,result if tickerSource.transport==\"peer\"then local requestId,err=requestPeerCraft(tickerSource,\"craft_request\",{name=tickerSource.name,destination=step.address,filters={{name=input.name,_requestCount=requested}}})if not requestId then failCraft(state,err)else active.packageRequest,active.phase={id=requestId,input=input,requested=requested,baseline=baseline,sent=now},\"await_peer_package\"end saveCraft(state);return else ok,result=peripherals.craftRequest(\"local\",tickerSource.name,step.address,{{name=input.name,_requestCount=input.count}})end local accepted,acceptErr=ok and crafting.acceptedCount(result,requested)or nil,result if not accepted then failCraft(state,acceptErr or\"Stock Ticker rejected package\")else active.issued,active.issuedKind,active.issuedAt=true,\"package\",now active.packageRequest={input=input,baseline=baseline,requested=requested,accepted=accepted,remaining=requested-accepted}active.phase,active.waitingFor,active.deadline=\"await_package_confirmation\",input.name,now+timeout state.nextRefresh=0 end end else if active.craftStep~=active.step then active.craftStep,active.pendingCraftRequests,active.craftBaseline,active.nextCraftDispatch=active.step,step.batches,crafting.normalise(state.stock)[step.name]or 0,0 end if now>=(active.nextCraftDispatch or 0)then local ok,result if requesterSource.transport==\"peer\"then if not active.peerRequest then local requestId,err=requestPeerCraft(requesterSource,\"craft_recipe_request\",{name=requesterSource.name,address=step.address,batches=1,grid=step.grid})if not requestId then failCraft(state,err)else active.peerRequest,active.phase={id=requestId,kind=\"craft\",sent=now},\"await_peer_craft\"end end saveCraft(state);return else ok,result=peripherals.craftingRecipeRequest(\"local\",requesterSource.name,step.address,1,step.grid)end if not ok then failCraft(state,result)else active.issued,active.issuedKind,active.issuedAt=true,\"craft\",now active.requesterConfigured=type(result)==\"table\"and result.configured==true active.requesterStrict=type(result)==\"table\"and result.strict==true active.pendingCraftRequests=active.pendingCraftRequests-1 active.nextCraftDispatch=now+100 if active.pendingCraftRequests<=0 then active.phase,active.baseline,active.waitingFor,active.expectedOutput,active.deadline=\"await_crafted_output\",active.craftBaseline,step.name,step.produced,now+timeout active.craftStep,active.craftBaseline,active.nextCraftDispatch=nil,nil,nil state.nextRefresh=0 end end end end elseif active.phase==\"await_peer_craft\"then local reply=takeCraftReply(active.peerRequest and active.peerRequest.id)if reply then active.peerRequest=nil if not reply.ok then failCraft(state,reply.result)else local step=active.plan[active.step]active.issued,active.issuedKind,active.issuedAt=true,\"craft\",now active.pendingCraftRequests=active.pendingCraftRequests-1 active.nextCraftDispatch=now+100 if active.pendingCraftRequests<=0 then active.phase,active.baseline,active.waitingFor,active.expectedOutput,active.deadline=\"await_crafted_output\",active.craftBaseline,step.name,step.produced,now+timeout;active.craftStep,active.craftBaseline,active.nextCraftDispatch=nil,nil,nil else active.phase=\"craft\"end end elseif active.peerRequest and now-(active.peerRequest.sent or now)>5000 then failCraft(state,\"Redstone Requester did not respond\")end elseif active.phase==\"await_peer_package\"then local pending=active.packageRequest local reply=takeCraftReply(pending and pending.id)if reply then active.packageRequest=nil local accepted,err=reply.ok and crafting.acceptedCount(reply.result,pending.requested or pending.input.count)or nil,reply.result if not accepted then failCraft(state,err or\"peer Stock Ticker rejected package\")else active.issued,active.issuedKind,active.issuedAt=true,\"package\",now active.packageRequest={input=pending.input,baseline=pending.baseline,requested=pending.requested or pending.input.count,accepted=accepted,remaining=(pending.requested or pending.input.count)-accepted}active.phase,active.waitingFor,active.deadline=\"await_package_confirmation\",pending.input.name,now+timeout state.nextRefresh=0 end elseif pending and now-(pending.sent or now)>5000 then failCraft(state,\"peer Stock Ticker did not respond to package request\")end elseif active.phase==\"await_package_confirmation\"then local package=active.packageRequest if not package then failCraft(state,\"lost package confirmation state\")elseif(crafting.normalise(state.stock)[package.input.name]or 0)<=package.baseline-package.accepted then if package.remaining and package.remaining>0 then active.packageRemaining,active.packageRequest,active.phase=package.remaining,nil,\"craft\"else active.packageIndex,active.packageRemaining,active.packageRequest,active.phase=(active.packageIndex or 1)+1,nil,nil,\"craft\"end else state.nextRefresh=0 end elseif active.phase==\"await_crafted_output\"then local step=active.plan[active.step]if(crafting.normalise(state.stock)[active.waitingFor]or 0)>=active.baseline+step.produced then active.step=active.step+1;active.phase=\"craft\"else state.nextRefresh=0 end elseif active.phase==\"deliver\"then local baseline=crafting.normalise(state.stock)[item.name]or 0 local ok,result if tickerSource.transport==\"peer\"then if not active.peerRequest then local requestId,err=requestPeerCraft(tickerSource,\"craft_request\",{name=tickerSource.name,destination=active.destination,filters={{name=item.name,_requestCount=item.count}}})if not requestId then failCraft(state,err)else active.peerRequest,active.phase={id=requestId,kind=\"delivery\",baseline=baseline,sent=now},\"await_peer_delivery\"end end saveCraft(state);return else ok,result=peripherals.craftRequest(\"local\",tickerSource.name,active.destination,{{name=item.name,_requestCount=item.count}})end local accepted,acceptError if ok then accepted,acceptError=crafting.acceptedCount(result,item.count)else acceptError=result end if not accepted then failCraft(state,acceptError or result)else active.requested,active.accepted,active.delivered,active.remaining=item.count,accepted,0,item.count-accepted active.issued,active.issuedKind,active.issuedAt=true,\"delivery\",now active.phase,active.baseline,active.waitingFor,active.deadline=\"await_delivery_confirmation\",baseline,item.name,now+timeout state.nextRefresh=0 end elseif active.phase==\"await_peer_delivery\"then local reply=takeCraftReply(active.peerRequest and active.peerRequest.id)if reply then local baseline=active.peerRequest.baseline;active.peerRequest=nil local accepted,err=reply.ok and crafting.acceptedCount(reply.result,item.count)or nil,reply.result if not accepted then failCraft(state,err or\"peer Stock Ticker rejected the request\")else active.requested,active.accepted,active.delivered,active.remaining=item.count,accepted,0,item.count-accepted active.issued,active.issuedKind,active.issuedAt=true,\"delivery\",now active.phase,active.baseline,active.waitingFor,active.deadline=\"await_delivery_confirmation\",baseline,item.name,now+timeout;state.nextRefresh=0 end end elseif active.phase==\"await_delivery_confirmation\"then if(crafting.normalise(state.stock)[active.waitingFor]or 0)<=active.baseline-active.accepted then active.delivered=active.accepted local itemResult={id=active.id,status=active.remaining>0 and\"partial-dispatched\"or\"dispatched\",name=item.name,requested=active.requested,accepted=active.accepted,dispatched=active.delivered,remaining=active.remaining,destination=active.destination,finished=os.epoch(\"utc\")}pushCraftHistory(state,itemResult)if active.remaining>0 then state.sequence=state.sequence+1 state.retries[#state.retries+1]={id=\"craft-retry-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(state.sequence),items={{name=item.name,count=active.remaining}},craftMode=active.craftMode,destination=active.destination,retryOf=active.id}end completeCraftItem(state)else state.nextRefresh=0 end end end saveCraft(state)end local function craftingStatus()store.invalidate(CRAFT_STATE)local state=store.read(CRAFT_STATE,craftDefault())local active,item=state.active,state.active and state.active.items and state.active.items[state.active.index]local history=state.history or{}return{active=active and{id=active.id,phase=active.phase,name=item and item.name,requested=active.requested or(item and item.count),accepted=active.accepted,dispatched=active.delivered,remaining=active.remaining,destination=active.destination,issued=active.issued,issuedKind=active.issuedKind,requesterConfigured=active.requesterConfigured,requesterStrict=active.requesterStrict,waitingFor=active.waitingFor,expectedOutput=active.expectedOutput,pendingCraftRequests=active.pendingCraftRequests,stepAddress=active.stepAddress,stepRoute=active.stepRoute,packageIndex=active.packageIndex,packageCount=active.packageCount}or nil,queue=#(state.queue or{}),retries=#(state.retries or{}),error=state.error,recoveryNotice=state.recoveryNotice,recipeServer=state.config and state.config.recipeServer,last=history[#history],stockUpdated=state.stockUpdated,}end local function craftingPeripheralStatus()local state=craftState()refreshCraftSources(state,false)local tickerSource,requesterSource=craftSources.kind(state.config.ticker),craftSources.kind(state.config.requester)local requester=requesterSource and requesterSource.transport~=\"peer\"and peripheral.wrap(requesterSource.name)or nil local encoded=nil if requester and type(requester.getRequest)==\"function\"then local ok,value=pcall(requester.getRequest)if ok and type(value)==\"table\"then for _ in pairs(value)do encoded=(encoded or 0)+1 end end end local configuration=requester and type(requester.getConfiguration)==\"function\"and select(2,pcall(requester.getConfiguration))or nil local address=requester and type(requester.getAddress)==\"function\"and select(2,pcall(requester.getAddress))or nil return{ticker=tickerSource and{name=tickerSource.name,transport=tickerSource.transport,peer=tickerSource.peer,available=sourceAvailable(tickerSource,\"stockticker\")}or nil,requester=requesterSource and{name=requesterSource.name,transport=requesterSource.transport,peer=requesterSource.peer,available=sourceAvailable(requesterSource,\"redstonerequester\"),configuration=configuration,address=address,encodedSlots=encoded}or nil,sources=state.sources}end local function processDevNetworkCommand()local command=networkIpc.take()if type(command)~=\"table\"then return end writeDevValue(DEV_NETWORK_IPC_DIAG,{id=command.id,action=command.action,phase=\"received\",at=os.epoch(\"utc\")})local ok,result,detail=pcall(function()if command.action==\"network_status\"then return net.statusSnapshot()elseif command.action==\"route_status\"then return net.routeStatus(command.target)elseif command.action==\"network_trace\"then return net.transportTrace(command.kind)elseif command.action==\"peripheral_status\"then return{localDevices=peripherals.listLocal(),sharedDevices=peripherals.describe(),crafting=craftingPeripheralStatus()}elseif command.action==\"craft_status\"then return craftingStatus()elseif command.action==\"update_status\"then auth.require(\"operator\");return updater.status()elseif command.action==\"update_check\"then auth.require(\"operator\");return updater.check()elseif command.action==\"update_offers\"then auth.require(\"operator\");return updater.status().offers elseif command.action==\"update_download\"then auth.require(\"operator\");return updater.download(command.source)elseif command.action==\"update_cancel\"then auth.require(\"operator\");return updater.cancel()elseif command.action==\"update_clear_cache\"then auth.require(\"operator\");return updater.clearCache()elseif command.action==\"update_apply\"then auth.require(\"operator\")local applied,detail=updater.prepareApply()if not applied then return false,detail end return{reboot=true,detail=detail}elseif command.action==\"discovery_start\"then auth.require(\"operator\");return net.startDiscovery(command.mode or\"peer\",command.master or\"initiator\",command.phrase)elseif command.action==\"discovery_join\"then auth.require(\"operator\");return net.joinDiscovery(command.phrase)elseif command.action==\"discovery_confirm\"then auth.require(\"operator\");return net.confirmDiscovery()elseif command.action==\"discovery_cancel\"then auth.require(\"operator\");return net.cancelDiscovery()elseif command.action==\"remote_request\"then auth.require(\"operator\");return net.request(command.target,command.type,command.body or{})elseif command.action==\"auth_send\"then if type(command.target)~=\"string\"or type(command.type)~=\"string\"or type(command.body)~=\"table\"then return false,\"invalid authority request\"end return net.request(command.target,command.type,command.body,{safe=true,confidential=true})end error(\"unknown development network command\")end)if not ok then detail,result=result,nil elseif result==true and detail~=nil then result,detail=detail,nil elseif result==false then ok,detail=false,detail or\"network command failed\"end local replied,replyErr=networkIpc.reply(command.id,{id=command.id,ok=ok,result=ok and result or nil,error=ok and nil or tostring(detail)})if not replied then error(\"could not write network IPC result: \"..tostring(replyErr),0)end writeDevValue(DEV_NETWORK_IPC_DIAG,{id=command.id,action=command.action,phase=\"replied\",ok=ok,at=os.epoch(\"utc\")})networkIpc.prune(32)writeDevValue(DEV_NETWORK_STATUS,net.statusSnapshot())end math.randomseed(os.epoch(\"utc\"))auth.bootstrap()net.start()updater.recover()shell.setPath(\"/ceetos/bin:\"..shell.path())shell.setAlias(\"craftos-programs\",\"/rom/programs/programs.lua\")shell.setAlias(\"craftos-list\",\"/rom/programs/list.lua\")shell.setAlias(\"craftos-delete\",\"/rom/programs/delete.lua\")shell.setAlias(\"craftos-edit\",\"/rom/programs/edit.lua\")shell.setAlias(\"craftos-shell\",\"/ceetos/bin/craftos-shell.lua\")shell.setAlias(\"devices\",\"/ceetos/bin/ceetdevices.lua\")shell.setAlias(\"shareall\",\"/ceetos/bin/ceetshareall.lua\")shell.setAlias(\"unshareall\",\"/ceetos/bin/ceetunshareall.lua\")shell.setAlias(\"craft\",\"/ceetos/bin/ceetcraft.lua\")local function commandArgs(command)local result={}for token in tostring(command or\"\"):gmatch(\"%S+\")do result[#result+1]=token end return result end local function runCaptured(command)local output,old={},term.current()local capture={}function capture.write(text)output[#output+1]=tostring(text or\"\")end function capture.blit(text)output[#output+1]=tostring(text or\"\")end function capture.setCursorPos()end function capture.setCursorBlink()end setmetatable(capture,{__index=old})term.redirect(capture)local args=commandArgs(command)local ok,result=pcall(function()return shell.run(args[1],table.unpack(args,2))end)term.redirect(old)return ok,result,table.concat(output)end local function jobStatus()local state=jobs.state()local running=0;for _ in pairs(state.running or{})do running=running+1 end return{label=os.getComputerLabel(),capacity=state.capacity or 1,inFlight=running,queueDepth=#(state.queue or{}),available=true}end local function writeJobInbox(job)local inbox=store.read(JOB_INBOX,{})inbox[#inbox+1]=job while#inbox>16 do table.remove(inbox,1)end store.write(JOB_INBOX,inbox)end local function workerJobLoop()while true do store.invalidate(JOB_INBOX)local inbox=store.read(JOB_INBOX,{})local job=table.remove(inbox,1)if job then store.write(JOB_INBOX,inbox)local template=jobs.listTemplates()[job.template]local valid=template and template.revision==job.revision and template.command==job.command local ok,result,output=false,\"template mismatch\",\"\"if valid then ok,result,output=runCaptured(job.command)end local outbox=store.read(JOB_OUTBOX,{})outbox[#outbox+1]={id=job.id,controller=job.controller,ok=valid and ok and result~=false,output=output,error=valid and(ok and nil or tostring(result))or result}while#outbox>16 do table.remove(outbox,1)end store.write(JOB_OUTBOX,outbox)else sleep(0.15)end end end local function chooseWorker()local choices={}for workerId,worker in pairs(jobs.workers())do if worker.healthy and(worker.slots or 0)>0 then choices[#choices+1]=worker end end table.sort(choices,function(a,b)local aLoad,bLoad=(a.inFlight or 0)/math.max(1,a.capacity or 1),(b.inFlight or 0)/math.max(1,b.capacity or 1)if aLoad~=bLoad then return aLoad<bLoad end if(a.hops or 0)~=(b.hops or 0)then return(a.hops or 0)<(b.hops or 0)end return tostring(a.id)<tostring(b.id)end)return choices[1]end local function syncJobTemplates()if tostring(net.controller())~=tostring(os.getComputerID())then return end local payload={controller=net.controller(),templates=jobs.listTemplates()}for _,peer in ipairs(net.peers())do net.request(peer.id,\"job_sync\",payload)end end local function processJobOutbox()store.invalidate(JOB_OUTBOX)local outbox=store.read(JOB_OUTBOX,{})if#outbox==0 then return end store.write(JOB_OUTBOX,{})for _,row in ipairs(outbox)do if tostring(row.controller)==tostring(os.getComputerID())then if row.ok then pcall(jobs.complete,row.id,{output=row.output,ok=true})else pcall(jobs.fail,row.id,row.error or\"worker failed\")end else net.request(row.controller,\"job_result\",row)end end end local function dispatchJobs()if tostring(jobs.controller())~=tostring(net.controller())then jobs.setController(net.controller())end jobs.heartbeat(os.getComputerID(),jobStatus())if tostring(net.controller())~=tostring(os.getComputerID())then net.request(net.controller(),\"job_heartbeat\",jobStatus(),{safe=true})return end jobs.tick()if os.epoch(\"utc\")-lastJobTemplateSync>=10000 then syncJobTemplates();lastJobTemplateSync=os.epoch(\"utc\")end while true do local queued,worker=jobs.nextDispatch(),chooseWorker()if not queued or not worker then break end local ok,assigned=pcall(jobs.assign,queued,worker.id)if not ok then break end assigned.controller=tostring(os.getComputerID())if tostring(worker.id)==tostring(os.getComputerID())then writeJobInbox(assigned)else local sent,err=net.request(worker.id,\"job_dispatch\",assigned)if not sent then pcall(jobs.fail,assigned.id,err)end end end end local function syncAccounts()if auth.centralActive and auth.centralActive()then return end local snapshot=auth.snapshot()for _,peer in ipairs(net.directPeers())do net.request(peer.id,\"account_sync\",snapshot)end end net.publicHandler=function(packet,distance)local ok,err=pcall(updater.publicPacket,packet,distance)if not ok then audit.log(\"system\",\"update.public_packet_error\",{error=tostring(err):sub(1,120)})end end net.handler=function(from,peer,packet)if packet.type==\"update_query\"or packet.type==\"update_offer\"or packet.type==\"update_chunk_request\"or packet.type==\"update_chunk\"then local ok,err=pcall(updater.authenticatedPacket,from,peer,packet)if not ok then audit.log(\"system\",\"update.packet_error\",{error=tostring(err):sub(1,120)})end return end if packet.type==\"announce\"then print(\"[peer] \"..from..\" is online\"..((packet.body and packet.body.label)and(\" (\"..packet.body.label..\")\")or\"\"))elseif packet.type==\"profile_query\"then net.reply(packet,\"profile_status\",{profile=profile.id(),version=version.string},{safe=true})elseif packet.type==\"profile_status\"then recordProfile(packet.origin or from,packet.body)elseif packet.type==\"peripheral_list\"then net.reply(packet,\"peripheral_list_result\",peripherals.describe(),{safe=true})elseif packet.type==\"peripheral_list_result\"then local origin=packet.origin or from local cache=store.read(\"/ceetos/data/remote/\"..origin..\".lua\",{})cache.peripherals,cache.updated=packet.body or{},os.epoch(\"utc\")store.write(\"/ceetos/data/remote/\"..origin..\".lua\",cache)elseif packet.type==\"peripheral_call\"then local body=packet.body or{}local ok,result=peripherals.call(peer.master,body.name,body.method,body.args)net.reply(packet,\"peripheral_result\",{ok=ok,result=result})elseif packet.type==\"peripheral_result\"then local origin=packet.origin or from local cache=store.read(\"/ceetos/data/remote/\"..origin..\".lua\",{})cache.last_result,cache.updated=packet.body,os.epoch(\"utc\")store.write(\"/ceetos/data/remote/\"..origin..\".lua\",cache)elseif packet.type==\"craft_stock_request\"then local body=packet.body or{}local origin=tostring(packet.origin or from)local ok,result=peripherals.call(peer.master,body.name,\"stock\",{true})net.reply(packet,\"craft_stock_result\",{request_id=body.request_id,ok=ok,result=result},{safe=true})elseif packet.type==\"craft_stock_result\"then local body=packet.body or{}if type(body.request_id)==\"string\"then local replies=store.read(CRAFT_REPLY_PATH,{})replies[body.request_id]={ok=body.ok==true,result=body.result,received=os.epoch(\"utc\"),from=packet.origin or from}store.write(CRAFT_REPLY_PATH,replies)end elseif packet.type==\"recipe_plan_request\"then local body=packet.body or{}local valid=type(body.request_id)==\"string\"and#body.request_id<=120 and type(body.item)==\"string\"and#body.item<=128 and tonumber(body.quantity)and tonumber(body.quantity)>=1 and tonumber(body.quantity)<=256 and type(body.stock)==\"table\"local stockCount=0 for _,row in pairs(type(body.stock)==\"table\"and body.stock or{})do stockCount=stockCount+1 if stockCount>512 or type(row)~=\"table\"or type(row.name or row.id)~=\"string\"or tonumber(row.count or row.amount or row.quantity or 0)==nil then valid=false;break end end local ok,result=false,\"invalid recipe plan request\"if valid then local recipes,tags,err=craftRecipes()if recipes then local plan,planErr=crafting.plan(recipes,body.stock,body.item,math.floor(tonumber(body.quantity)),{forceRoot=true,tags=tags})if plan and#plan<=96 then local encoded=textutils.serialise(plan)if#encoded<=12*1024 then ok,result=true,plan else result=\"recipe plan exceeds remote reply limit\"end else result=planErr or\"recipe plan exceeds remote reply limit\"end else result=err or\"recipe database is unavailable\"end end net.reply(packet,\"recipe_plan_result\",{request_id=body.request_id,ok=ok,result=result},{safe=true})elseif packet.type==\"recipe_plan_result\"then local body=packet.body or{}if type(body.request_id)==\"string\"then local replies=store.read(CRAFT_REPLY_PATH,{})replies[body.request_id]={ok=body.ok==true,result=body.result,received=os.epoch(\"utc\"),from=packet.origin or from}store.write(CRAFT_REPLY_PATH,replies)end elseif packet.type==\"craft_request\"then local body=packet.body or{}local origin=tostring(packet.origin or from)local allowed=net.isDirectController(from,packet)and(peer.role==\"admin\"or peer.role==\"operator\")local ok,result=false,\"craft request is not authorised\"if allowed then ok,result=peripherals.craftRequest(peer.master,body.name,body.destination,body.filters)end net.reply(packet,\"craft_result\",{request_id=body.request_id,ok=ok,result=result})elseif packet.type==\"craft_recipe_request\"then local body=packet.body or{}local origin=tostring(packet.origin or from)local allowed=net.isDirectController(from,packet)and(peer.role==\"admin\"or peer.role==\"operator\")local ok,result=false,\"craft recipe request is not authorised\"if allowed then ok,result=peripherals.craftingRecipeRequest(peer.master,body.name,body.address,body.batches,body.grid)end net.reply(packet,\"craft_result\",{request_id=body.request_id,ok=ok,result=result})elseif packet.type==\"craft_result\"then local body=packet.body or{}if type(body.request_id)==\"string\"then local replies=store.read(CRAFT_REPLY_PATH,{})replies[body.request_id]={ok=body.ok==true,result=body.result,received=os.epoch(\"utc\"),from=packet.origin or from}local count=0;for _ in pairs(replies)do count=count+1 end if count>32 then local oldest,oldestTime for key,row in pairs(replies)do if not oldestTime or(row.received or 0)<oldestTime then oldest,oldestTime=key,row.received or 0 end end if oldest then replies[oldest]=nil end end store.write(CRAFT_REPLY_PATH,replies)end elseif packet.type==\"shell_request\"then if peer.role~=\"admin\"and peer.role~=\"operator\"then return end local command=packet.body and packet.body.command if type(command)~=\"string\"or#command>240 or command:match(\"^%s*nx%s+recover\")then return end audit.log(\"peer:\"..from,\"shell.request\",{command=command,session=packet.body and packet.body.session})print(\"[remote \"..from..\"] \"..command)local ok,result,output=runCaptured(command)if packet.body and packet.body.session then net.reply(packet,\"shell_result\",{session=packet.body.session,ok=ok and result~=false,output=output,error=ok and nil or tostring(result)})end elseif packet.type==\"shell_result\"then local replies=store.read(SHELL_REPLY_PATH,{})replies[#replies+1]={from=from,body=packet.body or{},received=os.epoch(\"utc\")}while#replies>20 do table.remove(replies,1)end store.write(SHELL_REPLY_PATH,replies)elseif packet.type==\"auth_seed_request\"then net.reply(packet,\"auth_seed\",{snapshot=auth.authoritySnapshot(),request_id=packet.body and packet.body.request_id},{safe=true,confidential=true})elseif packet.type==\"auth_authority\"then local ok,err=pcall(auth.acceptAuthority,packet.body)if not ok then audit.log(\"system\",\"auth.authority_rejected\",{error=tostring(err):sub(1,120)})end elseif packet.type==\"auth_login_begin_result\"or packet.type==\"auth_login_proof_result\"or packet.type==\"auth_mutate_result\"then require((\"ceetos.lib.auth_client\")).receive(packet.body)elseif packet.type==\"account_sync\"then local changed,mergeError=auth.merge(packet.body)if changed then audit.log(\"peer:\"..from,\"accounts.sync\",{});syncAccounts()elseif mergeError then audit.log(\"peer:\"..from,\"accounts.sync_rejected\",{})end elseif packet.type==\"job_heartbeat\"then if tostring(net.controller())==tostring(os.getComputerID())then local body=packet.body or{};body.hops=1 jobs.heartbeat(from,body)end elseif packet.type==\"job_sync\"then local body=packet.body or{}if net.isDirectController(from,packet)and tostring(body.controller)==tostring(net.controller())and type(jobs.syncTemplates)==\"function\"then jobs.syncTemplates(body.controller,body.templates or{})end elseif packet.type==\"job_dispatch\"then local body=packet.body or{}if not net.isDirectController(from,packet)or tostring(body.controller)~=tostring(net.controller())then audit.log(\"peer:\"..from,\"jobs.rejected\",{reason=\"not_controller\"});return end local template=jobs.listTemplates()[body.template]if not template or template.revision~=body.revision or template.command~=body.command then audit.log(\"peer:\"..from,\"jobs.rejected\",{reason=\"template_mismatch\"});return end writeJobInbox(body)elseif packet.type==\"job_result\"then if tostring(net.controller())==tostring(os.getComputerID())then local body=packet.body or{}if body.ok then pcall(jobs.complete,body.id,{output=body.output,ok=true})else pcall(jobs.fail,body.id,body.error or\"worker failed\")end end end end local function serviceLoop()local timer,pollTimer,discoveryTimer,accountTimer,jobTimer,craftTimer,updateTimer,cloudTimer=os.startTimer(60),os.startTimer(5),os.startTimer(2),os.startTimer(2),os.startTimer(2),os.startTimer(0.1),os.startTimer(1),os.startTimer(5)local started,lastError=os.epoch(\"utc\"),nil local function health(errorText)if errorText then lastError=tostring(errorText):sub(1,240)end writeDevValue(DEV_NETWORK_HEALTH,{started=started,lastEvent=os.epoch(\"utc\"),healthy=lastError==nil,error=lastError,ipc=networkIpc.status()})end health()while true do local event={os.pullEventRaw()}local handled,handleErr=pcall(net.handle,event)if not handled then health(handleErr)audit.log(\"system\",\"network.packet_error\",{error=tostring(handleErr):sub(1,120)})else health()end local ipcOk,ipcErr=pcall(processDevNetworkCommand)if not ipcOk then health(ipcErr)audit.log(\"system\",\"network.ipc_error\",{error=tostring(ipcErr):sub(1,120)})end if event[1]==\"timer\"and event[2]==discoveryTimer then local tickOk,tickErr=pcall(net.tick)if not tickOk then health(tickErr)audit.log(\"system\",\"network.tick_error\",{error=tostring(tickErr):sub(1,120)})end local snapshotOk,snapshot=pcall(net.statusSnapshot)if snapshotOk then local wrote,writeErr=writeDevValue(DEV_NETWORK_STATUS,snapshot)if not wrote then health(writeErr)audit.log(\"system\",\"network.status_persist_error\",{error=tostring(writeErr):sub(1,120)})end else health(snapshot)audit.log(\"system\",\"network.status_error\",{error=tostring(snapshot):sub(1,120)})end discoveryTimer=os.startTimer(2)end if event[1]==\"timer\"and event[2]==timer then telemetry.post(\"shell\")timer=os.startTimer(60)end if event[1]==\"timer\"and event[2]==pollTimer then telemetry.poll(function(job)if type(job)~=\"table\"or type(job.id)~=\"string\"then return end if job.kind==\"owner_service\"then local ok,result=ownerService.dispatch(job.request)audit.log(\"owner-service\",ok and\"request.accepted\"or\"request.rejected\",{job=job.id,action=type(job.request)==\"table\"and job.request.action or nil})telemetry.postResult(job.id,{ok=ok,result=ok and result or nil,error=ok and nil or tostring(result)})return end local c=store.read(\"/ceetos/data/config.lua\",{})if not c.dashboard_terminal or type(job.command)~=\"string\"then return end audit.log(\"dashboard\",\"shell.request\",{command=job.command,job=job.id})local ok,result if job.command:match(\"^%s*nx%s+recover\")then ok,result=false,\"recovery commands are local-only\"else print(\"[dashboard] \"..job.command)local commandOk,commandResult=runCaptured(job.command)ok,result=commandOk,commandResult end telemetry.postResult(job.id,{ok=ok and result~=false,error=ok and nil or tostring(result)})end)pollTimer=os.startTimer(5)end if event[1]==\"timer\"and event[2]==accountTimer then syncAccounts()if os.epoch(\"utc\")-lastProfileAdvert>=30000 then for _,peer in ipairs(net.peers())do net.request(peer.id,\"profile_status\",{profile=profile.id(),version=version.string},{safe=true})end lastProfileAdvert=os.epoch(\"utc\")end accountTimer=os.startTimer(10)end if event[1]==\"timer\"and event[2]==jobTimer then processJobOutbox()local ok,err=pcall(dispatchJobs)if not ok then audit.log(\"system\",\"jobs.scheduler_error\",{error=tostring(err)})end jobTimer=os.startTimer(2)end if event[1]==\"timer\"and event[2]==craftTimer then local ok,err=pcall(tickCraftQueue)if not ok then local state=craftState()failCraft(state,\"crafting queue service error: \"..tostring(err))saveCraft(state)audit.log(\"system\",\"crafting.queue_error\",{error=tostring(err)})end craftTimer=os.startTimer(0.1)end if event[1]==\"timer\"and event[2]==updateTimer then local ok,err=pcall(updater.tick)if not ok then health(err)audit.log(\"system\",\"update.service_error\",{error=tostring(err):sub(1,120)})end updateTimer=os.startTimer(1)end if event[1]==\"timer\"and event[2]==cloudTimer then local ok,err=pcall(cloudSync.tick,\"desktop\")if not ok then audit.log(\"system\",\"cloud.sync_error\",{error=tostring(err):sub(1,120)})end cloudTimer=os.startTimer(5)end end end local function userDesktop()print(\"CeetOS \"..version.string..\" | computer \"..os.getComputerID()..\" -- starting Desktop\")while true do if fs.exists(\"/ceetos/data/desktop-stop\")then fs.delete(\"/ceetos/data/desktop-stop\")end local ok=shell.run(\"/ceetos/bin/ceetui.lua\")if ok==false and not fs.exists(\"/ceetos/data/desktop-stop\")then printError(\"CeetUI stopped; restarting in 2 seconds...\")sleep(2)elseif fs.exists(\"/ceetos/data/desktop-stop\")then fs.delete(\"/ceetos/data/desktop-stop\")return end end end local function devBridgeLoop()local path=\"/ceetos-dev/connection.lua\"while true do if fs.exists(path)then local handle=fs.open(path,\"r\")local cfg=handle and textutils.unserialise(handle.readAll())or nil if handle then handle.close()end if type(cfg)==\"table\"and type(cfg.url)==\"string\"then local token=type(cfg.token)==\"string\"and cfg.token or\"\"if token~=\"\"then pcall(shell.run,\"/ceetos/bin/ceetdev.lua\",cfg.url,token)else pcall(shell.run,\"/ceetos/bin/ceetdev.lua\",cfg.url)end sleep(3)else sleep(5)end else sleep(5)end end end parallel.waitForAny(serviceLoop,workerJobLoop,userDesktop,devBridgeLoop)",
  ["ceetos/bin/nx.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local net=require(\"ceetos.lib.net\")local peripherals=require(\"ceetos.lib.peripherals\")local telemetry=require(\"ceetos.lib.telemetry\")local cloudSync=require(\"ceetos.lib.cloud_sync\")local ownerService=require(\"ceetos.lib.owner_service\")local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local jobs=require(\"ceetos.lib.jobs\")local networkIpc=require(\"ceetos.lib.network_ipc\")local args={...}local requestSequence=0 local function networkRequest(action,body)requestSequence=requestSequence+1 local request=body or{}request.id=\"nx-\"..tostring(os.getComputerID())..\"-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(requestSequence)request.action=action local submitted,submitErr=networkIpc.submit(request)if not submitted then return false,submitErr or\"CeetOS network service is unavailable\"end local deadline=os.epoch(\"utc\")+5000 while os.epoch(\"utc\")<deadline do local response=networkIpc.poll(request.id,true)if response and response.id==request.id then return response.ok==true,response.result or response.error end sleep(0.1)end return false,\"CeetOS network service did not respond\"end local function usage()print(\"CeetOS: help, gui, craft, whoami, login, logout, users, user add/edit/delete, discover, network, peers, share, unshare, peripherals, remote, jobs, update, telemetry, cloud status|enroll <code>|retry|clear, owner-service, dashboard, recover\")end local function requireArgs(n)if#args<n then error(\"missing argument\",0)end end local command=args[1]or\"help\"if command==\"help\"then usage()elseif command==\"gui\"then shell.run(\"ceetui\")elseif command==\"craft\"then shell.run(\"ceetcraft\",table.unpack(args,2))elseif command==\"whoami\"then print(auth.current and(auth.current.name..\" (\"..auth.current.role..\")\")or\"not logged in\")elseif command==\"login\"then requireArgs(2);term.write(\"Password: \");local pass=read(\"*\")if auth.login(args[2],pass)then print(\"Logged in.\")else printError(\"Invalid credentials.\")end elseif command==\"logout\"then auth.logout();print(\"Logged out.\")elseif command==\"users\"then auth.require(\"operator\");for _,user in ipairs(auth.list())do print(user.name..\"\\t\"..user.role)end elseif command==\"user\"and args[2]==\"add\"then auth.require(\"operator\");requireArgs(4);term.write(\"Password for \"..args[3]..\": \");local pass=read(\"*\")auth.add(args[3],args[4],pass);audit.log(auth.current.name,\"user.add\",{name=args[3],role=args[4]});print(\"User created.\")elseif command==\"user\"and args[2]==\"edit\"then auth.require(\"operator\");requireArgs(4);auth.setRole(args[3],args[4]);audit.log(auth.current.name,\"user.role\",{name=args[3],role=args[4]});print(\"User role updated.\")elseif command==\"user\"and args[2]==\"delete\"then auth.require(\"operator\");requireArgs(3);auth.remove(args[3]);audit.log(auth.current.name,\"user.delete\",{name=args[3]});print(\"User deleted.\")elseif command==\"peers\"then local ok,result=networkRequest(\"network_status\",{})if not ok then printError(result)else for _,peer in ipairs(result.peers or{})do print(peer.id..\"\\t\"..peer.role..(peer.master and\"\\tmaster\"or\"\")..(peer.routed and\"\\trouted\"or\"\"))end end elseif command==\"discover\"then auth.require(\"operator\")if args[2]==\"status\"then local ok,result=networkRequest(\"network_status\",{});if not ok then printError(result)else print(textutils.serialize(result.discovery or{}))end elseif args[2]==\"cancel\"then local ok,result=networkRequest(\"discovery_cancel\",{});if ok then print(\"Discovery cancelled.\")else printError(result)end elseif args[2]==\"join\"then local ok,result=networkRequest(\"discovery_join\",{phrase=args[3]})if not ok then printError(result)elseif type(result)==\"table\"and result.state==\"candidate\"then print(\"Matching computer found. Run 'nx discover confirm' to pair.\")else print(\"Searching for a matching discovery session.\")end elseif args[2]==\"confirm\"then local ok,err=networkRequest(\"discovery_confirm\",{});if not ok then printError(err)else print(\"Pairing confirmation sent.\")end elseif args[2]==\"start\"then requireArgs(3)local phrase=args[3];local mode=args[4]==\"master\"and\"master\"or\"peer\";local master=args[5]or\"initiator\"local ok,err=networkRequest(\"discovery_start\",{phrase=phrase,mode=mode,master=master})if not ok then printError(err)else print(\"Discovery started. Keep this phrase private and have the other computer enter it.\")end else printError(\"use discover start <phrase> [peer|master] [initiator|joiner], join <phrase>, confirm, cancel, or status\")end elseif command==\"network\"and args[2]==\"status\"then local ok,result=networkRequest(\"network_status\",{});if not ok then printError(result)else print(textutils.serialize(result.transport or{}))end elseif command==\"network\"and args[2]==\"routes\"then local ok,result=networkRequest(\"route_status\",{target=args[3]});if not ok then printError(result)else for _,route in ipairs(result or{})do print(tostring(route.id)..\" via \"..tostring(route.next)..\" / \"..tostring(route.hops)..\" hop [\"..tostring(route.health)..\"]\"..(route.reason and(\" - \"..tostring(route.reason))or\"\"))end end elseif command==\"network\"and args[2]==\"trace\"then local ok,result=networkRequest(\"network_trace\",{kind=args[3]});if not ok then printError(result)else print(textutils.serialize(result))end elseif command==\"update\"then auth.require(\"operator\")local sub=args[2]or\"status\"if sub==\"status\"then local ok,result=networkRequest(\"update_status\",{})if not ok then printError(result)else print(\"CeetOS \"..tostring(result.version)..\" | \"..tostring(result.status or\"idle\"))if result.message then print(tostring(result.message))end if result.cache then print(\"Cached release: \"..tostring(result.cache.version)..\"  \"..tostring(result.cache.fingerprint)..\"  key \"..tostring(result.cache.keyId))end if result.transfer then print(\"Download: \"..tostring(result.transfer.received or 0)..\"/\"..tostring(result.transfer.size)..\" bytes from \"..tostring(result.transfer.label or result.transfer.source))end end elseif sub==\"check\"then local ok,result=networkRequest(\"update_check\",{})if ok then print(\"Checking directly reachable installations and authenticated routes for signed releases...\")else printError(result)end elseif sub==\"offers\"then local ok,result=networkRequest(\"update_offers\",{})if not ok then printError(result)elseif#result==0 then print(\"No newer signed releases discovered.\")else for _,offer in ipairs(result)do print(tostring(offer.id)..\"\\t\"..tostring(offer.label)..\"\\t\"..tostring(offer.version)..\"\\t\"..tostring(offer.size)..\" bytes\\t\"..tostring(offer.fingerprint)..\"\\t\"..tostring(offer.keyId)..\"\\t\"..tostring(offer.transport))end end elseif sub==\"download\"then requireArgs(3)local ok,result=networkRequest(\"update_download\",{source=args[3]})if ok then print(\"Download started. Run 'nx update status' to follow verified progress.\")else printError(result)end elseif sub==\"cancel\"then local ok,result=networkRequest(\"update_cancel\",{});if ok then print(\"Update download cancelled.\")else printError(result)end elseif sub==\"clear-cache\"then local ok,result=networkRequest(\"update_clear_cache\",{});if ok then print(\"Verified release cache cleared.\")else printError(result)end elseif sub==\"apply\"then local ok,status=networkRequest(\"update_status\",{})if not ok then printError(status)elseif not status.cache then printError(\"No verified signed release is cached.\")else print(\"Apply CeetOS \"..tostring(status.cache.version)..\" from verified cache\")print(\"Fingerprint: \"..tostring(status.cache.fingerprint)..\"  Key: \"..tostring(status.cache.keyId))term.write(\"Type APPLY to reboot and install locally: \")if read()~=\"APPLY\"then print(\"Cancelled.\")else local prepared,result=networkRequest(\"update_apply\",{})if not prepared then printError(result)else print(\"Verified update approved; rebooting locally.\");os.reboot()end end end else printError(\"use update status|check|offers|download <source>|apply|cancel|clear-cache\")end elseif command==\"pair\"then printError(\"legacy ID/code pairing is disabled. Use 'nx discover start <phrase>' and 'nx discover join <phrase>'.\")elseif command==\"share\"then auth.require(\"operator\");requireArgs(2)if args[2]==\"all\"then print(\"Shared \"..peripherals.shareAll(args[3]==\"master\"and\"master\"or\"peer\")..\" peripherals\")else peripherals.share(args[2],args[3]or\"peer\");print(\"Shared \"..args[2])end elseif command==\"unshare\"then auth.require(\"operator\");requireArgs(2)if args[2]==\"all\"then print(\"Unshared \"..peripherals.unshareAll()..\" peripherals\")else peripherals.unshare(args[2]);print(\"Unshared \"..args[2])end elseif command==\"peripherals\"then for _,p in ipairs(peripherals.describe())do print(p.name..\"\\t\"..p.type..\"\\t\"..p.mode)end elseif command==\"remote\"then auth.require(\"operator\");requireArgs(3)local peer,mode=args[2],args[3]if mode==\"peripherals\"then local ok,err=networkRequest(\"remote_request\",{target=peer,type=\"peripheral_list\",body={}});if not ok then printError(err)else print(\"Peripheral query sent. Run 'nx remote \"..peer..\" cache' after the reply arrives.\")end elseif mode==\"cache\"then local cache=store.read(\"/ceetos/data/remote/\"..peer..\".lua\",{})if not cache.peripherals then print(\"No peripheral cache for peer \"..peer..\"; run 'nx remote \"..peer..\" peripherals' first.\")else for _,p in ipairs(cache.peripherals)do print(p.name..\"\\t\"..p.type..\"\\t\"..p.mode)end end elseif mode==\"call\"then requireArgs(5)local values={}for i=6,#args do values[#values+1]=tonumber(args[i])or args[i]end local ok,err=networkRequest(\"remote_request\",{target=peer,type=\"peripheral_call\",body={name=args[4],method=args[5],args=values}})if not ok then printError(err)else print(\"Peripheral call sent; the result will be displayed and cached when it arrives.\")end else local cmd=table.concat(args,\" \",3);local ok,err=networkRequest(\"remote_request\",{target=peer,type=\"shell_request\",body={command=cmd}});if not ok then printError(err)else print(\"Request sent.\")end end elseif command==\"jobs\"then local sub=args[2]or\"status\"local function controllerOnly()auth.require(\"admin\")assert(tostring(net.controller())==tostring(os.getComputerID()),\"this computer is not the network job controller (\"..tostring(net.controller())..\")\")end if sub==\"status\"then auth.require(\"operator\")local state=jobs.state();print(\"Controller: \"..tostring(net.controller())..\" | queue: \"..#(state.queue or{}))for id,worker in pairs(jobs.workers())do print(tostring(id)..\"\\t\"..tostring(worker.inFlight or 0)..\"/\"..tostring(worker.capacity or 1)..\"\\t\"..(worker.healthy and\"healthy\"or\"stale\"))end elseif sub==\"history\"then auth.require(\"operator\")for _,item in ipairs(jobs.history())do print(item.id..\"\\t\"..item.template..\"\\t\"..item.status..\"\\t\"..tostring(item.result and(item.result.error or item.result.output)or\"\"):sub(1,100))end elseif sub==\"templates\"then auth.require(\"operator\")for name,item in pairs(jobs.listTemplates())do print(name..\"\\t\"..(item.enabled==false and\"off\"or\"on\")..\"\\t\"..(item.retrySafe and\"retry-safe\"or\"no-retry\"))end elseif sub==\"template\"and args[3]==\"add\"then controllerOnly();requireArgs(5);local commandLine=table.concat(args,\" \",5)local template=jobs.addTemplate(args[4],commandLine,{});audit.log(auth.current.name,\"jobs.template_add\",{name=template.name});print(\"Template added.\")elseif sub==\"template\"and args[3]==\"retry-safe\"then controllerOnly();requireArgs(5);jobs.setTemplateRetrySafe(args[4],args[5]==\"on\");print(\"Retry policy updated.\")elseif sub==\"template\"and args[3]==\"enable\"then controllerOnly();requireArgs(5);jobs.setTemplateEnabled(args[4],args[5]~=\"off\");print(\"Template state updated.\")elseif sub==\"run\"then controllerOnly();requireArgs(3);local job=jobs.enqueue(args[3]);print(\"Queued \"..job.id)elseif sub==\"schedule\"then controllerOnly();requireArgs(4);local schedule=jobs.schedule(args[3],tonumber(args[4]));print(\"Scheduled \"..schedule.id)elseif sub==\"cancel\"then controllerOnly();requireArgs(3);jobs.cancel(args[3]);print(\"Schedule cancelled.\")elseif sub==\"capacity\"then auth.require(\"admin\");requireArgs(3);print(\"Worker capacity: \"..jobs.setCapacity(tonumber(args[3])))elseif sub==\"controller\"then controllerOnly();requireArgs(3);print(\"Controller: \"..net.setController(args[3]))else printError(\"use jobs status|history|templates|template add <name> <fixed command>|template retry-safe <name> on|off|run <name>|schedule <name> <seconds>|cancel <id>|capacity <slots>|controller <id>\")end elseif command==\"telemetry\"then auth.require(\"admin\");requireArgs(2);if args[2]==\"status\"then print(telemetry.status()and\"enabled\"or\"disabled\")elseif args[2]==\"on\"or args[2]==\"off\"then telemetry.setEnabled(args[2]==\"on\");audit.log(auth.current.name,\"telemetry\",{enabled=args[2]==\"on\"});print(\"Telemetry \"..args[2])else error(\"use on, off, or status\",0)end elseif command==\"cloud\"then local sub=args[2]or\"status\"if sub==\"status\"then auth.require(\"operator\")local status=cloudSync.status()print(\"Cloud: \"..(status.enrolled and\"enrolled\"or\"not enrolled\")..\" | auth \"..(status.authOnline and\"online\"or\"offline\")..\" | recipes \"..(status.recipeOnline and\"online\"or\"offline\"))if status.node then print(\"Node: \"..status.node)end if status.lastError then print(\"Last error: \"..status.lastError)end print(\"Dashboard: https://dash.ceet.uk (Cloudflare Access)\")elseif sub==\"enroll\"then auth.require(\"admin\");requireArgs(3)local status,err=cloudSync.enroll(args[3]);if not status then printError(err)else audit.log(auth.current.name,\"cloud.enroll\",{node=status.node});print(\"Cloud node enrollment saved. Background sync is enabled.\")end elseif sub==\"retry\"then auth.require(\"operator\")local ok,err=cloudSync.tick(\"manual\");if ok then print(\"Cloud sync requested.\")else printError(err)end elseif sub==\"clear\"then auth.require(\"admin\")local ok,err=cloudSync.clear();if ok then audit.log(auth.current.name,\"cloud.clear\",{});print(\"Cloud enrollment removed.\")else printError(err)end else error(\"use cloud status|enroll <one-time-code>|retry|clear\",0)end elseif command==\"owner-service\"then auth.require(\"admin\")local sub=args[2]or\"status\"if sub==\"status\"then local status=ownerService.status()print(\"Owner service: \"..(status.enabled and\"enabled\"or\"disabled\")..\" | \"..(status.provisioned and\"provisioned\"or\"sealed\"))if status.owner then print(\"Owner: \"..status.owner)end print(\"Actions: \"..table.concat(status.actions or{},\", \"))elseif sub==\"on\"or sub==\"off\"then ownerService.setEnabled(sub==\"on\")audit.log(auth.current.name,\"owner-service.enabled\",{enabled=sub==\"on\"})print(\"Owner service \"..sub..\".\")elseif sub==\"set-token\"then term.write(\"Owner-service token: \");local token=read(\"*\")term.write(\"Repeat token: \");local repeatToken=read(\"*\")assert(token==repeatToken,\"tokens do not match\")ownerService.provision(token,auth.current.name)audit.log(auth.current.name,\"owner-service.provision\",{})print(\"Owner service provisioned. Configure the same secret in CEETOS_SERVICE_TOKENS on the dashboard.\")elseif sub==\"rotate-token\"then local token=ownerService.rotate(auth.current.name)audit.log(auth.current.name,\"owner-service.rotate\",{})print(\"New owner-service token (shown once): \"..token)print(\"Update CEETOS_SERVICE_TOKENS on the dashboard before making remote requests.\")else error(\"use owner-service status|on|off|set-token|rotate-token\",0)end elseif command==\"dashboard\"and args[2]==\"configure\"then auth.require(\"admin\");requireArgs(5)local c=store.read(\"/ceetos/data/config.lua\",{})c.dashboard_url,c.dashboard_poll_url,c.dashboard_token=args[3],args[4],args[5]c.dashboard_terminal=args[6]==\"terminal\"store.write(\"/ceetos/data/config.lua\",c)audit.log(auth.current.name,\"dashboard.configure\",{terminal=c.dashboard_terminal})print(\"Dashboard configured. Terminal polling is \"..(c.dashboard_terminal and\"enabled\"or\"disabled\")..\".\")elseif command==\"recover\"and args[2]==\"enable\"then auth.require(\"admin\");local minutes=tonumber(args[3])or 10;assert(minutes>0 and minutes<=60,\"minutes must be 1-60\")store.write(\"/ceetos/data/recovery.lua\",{until_ts=os.epoch(\"utc\")+minutes*60000});audit.log(auth.current.name,\"recovery.enable\",{minutes=minutes});print(\"Recovery access enabled for \"..minutes..\" minutes.\")elseif command==\"recover\"and args[2]==\"reset\"then requireArgs(3)local recovery=store.read(\"/ceetos/data/recovery.lua\",{})assert((recovery.until_ts or 0)>os.epoch(\"utc\"),\"recovery window is not active\")term.write(\"New password for \"..args[3]..\": \");local pass=read(\"*\")assert(#pass>=8,\"password must contain at least 8 characters\")auth.resetPassword(args[3],pass)audit.log(\"local-recovery\",\"password.reset\",{name=args[3]})print(\"Password reset. The recovery window remains active until it expires.\")else usage()end",
  ["ceetos/bin/ceetui.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local gui=require(\"ceetos.lib.gui\")local auth=require(\"ceetos.lib.auth\")local net=require(\"ceetos.lib.net\")local peripherals=require(\"ceetos.lib.peripherals\")local telemetry=require(\"ceetos.lib.telemetry\")local cloudSync=require(\"ceetos.lib.cloud_sync\")local audit=require(\"ceetos.lib.audit\")local store=require(\"ceetos.lib.store\")local files=require(\"ceetos.lib.files\")local clipboard=require(\"ceetos.lib.clipboard\")local networkIpc=require(\"ceetos.lib.network_ipc\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local release=require(\"ceetos.lib.release_verify\")local launch={...}local state=gui.newState()local page=launch[1]==\"files\"and\"files\"or\"home\"if page==\"files\"and launch[2]and fs.exists(launch[2])then local target=launch[2]state.path=fs.isDir(target)and target or fs.getDir(target)state.selected=\"file:\"..target end local banner,targets,currentModel=nil,{},nil local remotePending=nil local function deps()return{auth=auth,net=net,peripherals=peripherals,telemetry=telemetry,cloud=cloudSync,audit=audit,files=files}end local function refresh(message,error)banner=message and{text=message,error=error}or nil currentModel=gui.model(page,deps(),state)targets=gui.draw(currentModel,state,banner)end local function prompt(label,secret)local _,height=term.getSize()term.setCursorPos(1,height);term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clearLine()term.write(label..\": \")return read(secret and\"*\"or nil)end local function fill(x,y,width,height,background)return terminalUi.fill(x,y,width,height,background)end local function choose(label,options)if#options==0 then return nil end local selected=1 local function draw()gui.draw(currentModel,state,banner)local width,height=term.getSize();local boxWidth=math.max(28,math.min(width-4,42));local x=math.max(2,math.floor((width-boxWidth)/2));local y=math.max(3,math.floor((height-math.min(#options+4,height-4))/2))fill(x,y,boxWidth,math.min(#options+4,height-4),colors.black)term.setTextColor(colors.white);term.setCursorPos(x+2,y+1);term.write(label:sub(1,boxWidth-4))for index,option in ipairs(options)do if y+index+1<height then term.setBackgroundColor(index==selected and colors.gray or colors.black);term.setTextColor(colors.white);term.setCursorPos(x+2,y+index+1);term.write(tostring(option.label or option):sub(1,boxWidth-4))end end return x,y,boxWidth end local x,y,boxWidth=draw()while true do local event,first,second,third=os.pullEvent()if event==\"key\"then local key=keys.getName(first)if key==\"up\"then selected=math.max(1,selected-1);x,y,boxWidth=draw()elseif key==\"down\"then selected=math.min(#options,selected+1);x,y,boxWidth=draw()elseif key==\"enter\"then return options[selected].value or options[selected]elseif key==\"backspace\"then return nil end elseif event==\"mouse_click\"and second>=x and second<=x+boxWidth and third>=y+2 and third<=y+#options+1 then local index=third-y-1;if options[index]then return options[index].value or options[index]end elseif event==\"term_resize\"then x,y,boxWidth=draw()end end end local networkRequestCounter=0 local function networkRequest(action,payload)networkRequestCounter=networkRequestCounter+1 local request=payload or{}request.id=\"ui-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(networkRequestCounter)request.action=action local submitted,submitErr=networkIpc.submit(request)if not submitted then return false,submitErr or\"CeetOS network service is unavailable\"end local deadline=os.epoch(\"utc\")+5000 while os.epoch(\"utc\")<deadline do local result=networkIpc.poll(request.id,true)if result and result.id==request.id then return result.ok==true,result.result or result.error end sleep(0.1)end return false,\"CeetOS network service did not respond\"end local function displayRemoteResult(result)if type(result)~=\"table\"then return tostring(result or\"\")end if result.ok==false then return tostring(result.result or result.error or\"request failed\")end local value=result.result local text=type(value)==\"table\"and textutils.serialise(value,{compact=true})or tostring(value)return terminalUi.clean(text):sub(1,240)end local function confirm(label)local function draw()gui.draw(currentModel,state,banner)local width,height=term.getSize()local boxWidth=math.min(width-4,math.max(28,#label+4))local boxHeight,x,y=6,math.floor((width-boxWidth)/2)+1,math.max(3,math.floor((height-6)/2))fill(x,y,boxWidth,boxHeight,colors.black)term.setTextColor(colors.white);term.setCursorPos(x+2,y+1);term.write(label:sub(1,boxWidth-4))local confirmX,cancelX=x+2,x+math.floor(boxWidth/2)+1 term.setBackgroundColor(colors.green);term.setTextColor(colors.black);term.setCursorPos(confirmX,y+4);term.write(\" Confirm \")term.setBackgroundColor(colors.gray);term.setTextColor(colors.white);term.setCursorPos(cancelX,y+4);term.write(\" Cancel \")term.setBackgroundColor(colors.black);term.setTextColor(colors.lightGray);term.setCursorPos(x+2,y+2);term.write(\"Click a choice or press Enter / Backspace\")term.setTextColor(colors.white)return{confirmX=confirmX,cancelX=cancelX,y=y+4}end local modal=draw()while true do local event,first,second,third=os.pullEvent()if event==\"mouse_click\"and third==modal.y then if second>=modal.confirmX and second<=modal.confirmX+8 then return true end if second>=modal.cancelX and second<=modal.cancelX+7 then return false end elseif event==\"key\"then local key=keys.getName(first)if key==\"enter\"or key==\"y\"then return true end if key==\"backspace\"or key==\"n\"then return false end elseif event==\"char\"then if first:lower()==\"y\"then return true elseif first:lower()==\"n\"then return false end elseif event==\"term_resize\"then modal=draw()end end end local function chooseRole()local roles={\"viewer\",\"operator\",\"admin\"}local function draw()local width,height=term.getSize();local boxWidth=30;local x,y=math.max(2,math.floor((width-boxWidth)/2)),math.max(3,math.floor((height-8)/2))fill(x,y,boxWidth,8,colors.black)term.setTextColor(colors.white);term.setCursorPos(x+2,y+1);term.write(\"Choose account role\")for index,roleName in ipairs(roles)do term.setBackgroundColor(index==1 and colors.gray or colors.black);term.setTextColor(colors.white);term.setCursorPos(x+2,y+index+1);term.write(\"[\"..index..\"] \"..roleName)end term.setBackgroundColor(colors.black);term.setTextColor(colors.gray);term.setCursorPos(x+2,y+6);term.write(\"Click a role or press 1-3 / Backspace\")return x,y end local x,y=draw()while true do local event,first,second,third=os.pullEvent()if event==\"char\"then local index=tonumber(first);if index and roles[index]then return roles[index]end elseif event==\"mouse_click\"and second>=x+1 and second<=x+18 and third>=y+2 and third<=y+4 then return roles[third-y-1]elseif event==\"key\"and keys.getName(first)==\"backspace\"then return nil elseif event==\"term_resize\"then x,y=draw()end end end local function values(text)local out={}for item in(text or\"\"):gmatch(\"[^,]+\")do out[#out+1]=tonumber(item)or item end return out end local function openMenu(item)local items=gui.context(item)if#items==0 then return refresh(\"No actions available for \"..tostring(item and item.label or\"item\"),true)end state.menu={items=items,selected=1}refresh()end local function cacheFor(peer)local cachePath=\"/ceetos/data/remote/\"..tostring(peer)..\".lua\"store.invalidate(cachePath)return store.read(cachePath,{}),cachePath end local function beginRemoteRequest(kind,peer,body,continueCall)networkRequestCounter=networkRequestCounter+1 local requested=os.epoch(\"utc\")local request={id=\"ui-remote-\"..tostring(requested)..\"-\"..tostring(networkRequestCounter),action=\"remote_request\",target=peer,type=kind==\"list\"and\"peripheral_list\"or\"peripheral_call\",body=body,}local sent,err=networkIpc.submit(request)if not sent then state.remoteResult={status=\"error\",error=true,peer=tostring(peer),text=err or\"CeetOS network service is unavailable\"}return false end local cache,cachePath=cacheFor(peer)remotePending={kind=kind,peer=peer,body=body,continueCall=continueCall==true,id=request.id,requested=requested,cachePath=cachePath,phase=\"dispatch\",deadline=requested+5000,cache=cache}state.remoteResult={status=\"pending\",pending=true,peer=tostring(peer),device=body and body.name,method=body and body.method,text=kind==\"list\"and\"Requesting peripheral list...\"or\"Calling remote peripheral...\"}return true end local function chooseRemoteCall(peer,cache)local choices={}for _,item in ipairs(cache.peripherals or{})do choices[#choices+1]={label=tostring(item.name)..\" (\"..tostring(item.type or\"unknown\")..\")\",value=item}end local device=choose(\"Choose peripheral\",choices);if not device then return false end local methods={};for _,method in ipairs(device.methods or{})do methods[#methods+1]={label=method,value=method}end local method=choose(\"Choose function\",methods);if not method then return false end return beginRemoteRequest(\"call\",peer.id or peer,{name=device.name,method=method,args=values(prompt(\"Arguments (comma separated)\"))})end local function pollRemoteRequest()if not remotePending then return false end local pending,now=remotePending,os.epoch(\"utc\")if now>=pending.deadline then state.remoteResult={status=\"error\",error=true,peer=tostring(pending.peer),device=pending.body and pending.body.name,method=pending.body and pending.body.method,text=pending.kind==\"list\"and\"No peripheral-list reply received\"or\"No peripheral-call reply received\"}remotePending=nil return true end if pending.phase==\"dispatch\"then local result=networkIpc.poll(pending.id,true)if result then if result.ok~=true then state.remoteResult={status=\"error\",error=true,peer=tostring(pending.peer),text=tostring(result.error or\"Remote request was rejected\")}remotePending=nil return true end pending.phase=\"reply\"return true end return false end store.invalidate(pending.cachePath)local cache=store.read(pending.cachePath,{})if cache.updated and cache.updated>=pending.requested and(pending.kind~=\"list\"or type(cache.peripherals)==\"table\")and(pending.kind~=\"call\"or cache.last_result~=nil)then remotePending=nil if pending.kind==\"list\"and pending.continueCall then state.remoteResult={status=\"ready\",peer=tostring(pending.peer),text=\"Peripheral list received. Choose a device.\"}chooseRemoteCall({id=pending.peer},cache)elseif pending.kind==\"list\"then state.remoteResult={status=\"ready\",peer=tostring(pending.peer),text=tostring(#(cache.peripherals or{}))..\" shared peripherals available\"}else local result=cache.last_result state.remoteResult={status=result and result.ok==false and\"error\"or\"complete\",error=result and result.ok==false or false,peer=tostring(pending.peer),device=pending.body and pending.body.name,method=pending.body and pending.body.method,text=displayRemoteResult(result)}end return true end return false end local function run(action)state.menu=nil if action:sub(1,5)==\"page:\"then page=action:sub(6);state.selected=nil;return refresh()end if action==\"terminal\"then term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)local ok,result=pcall(shell.run,\"/ceetos/bin/ceetshell.lua\")if not ok or result==false then return refresh(\"Terminal failed to start: \"..tostring(result or\"runtime error\"),true)end return refresh()end if action==\"crafting\"then term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)local ok,result=pcall(shell.run,\"/ceetos/bin/ceetcraft.lua\")if not ok or result==false then return refresh(\"Crafting console failed to start\",true)end return refresh()end if action==\"peripheral_view\"then state.peripheralView=true;state.selected=nil;return refresh()end if action==\"trash_open\"then state.trash=true;state.selected=nil;return refresh()end if action:sub(1,3)==\"up:\"then state.path=table.remove(state.history)or action:sub(4);state.selected=nil;return refresh()end if action:sub(1,4)==\"dir:\"then state.history[#state.history+1]=state.path;state.path=action:sub(5);state.selected=nil;return refresh()end if action:sub(1,4)==\"run:\"then if not auth.allowed(\"viewer\")then return refresh(\"Login required to run programs\",true)end local path=action:sub(5)if not files.runnable(path)then return refresh(\"This file is not a runnable Lua program\",true)end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)shell.run(\"/ceetos/bin/ceetlaunch.lua\",path)return refresh()end if action:sub(1,5)==\"edit:\"then local path=action:sub(6)term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)shell.run(\"/ceetos/bin/ceetedit.lua\",path)return refresh()end local ok,result=pcall(function()if action:sub(1,11)==\"copy_label:\"then local id=action:sub(12);local item=currentModel and gui.selected(currentModel,state)if item and item.id==id then clipboard.set(item.label or\"\");return\"Copied\"end return\"Nothing selected\"elseif action:sub(1,11)==\"copy_text:\"then clipboard.set(action:sub(12));return\"Copied\"elseif action==\"remote_result_copy\"then local result=state.remoteResult if not result or not result.text then return\"No remote result to copy\"end clipboard.set(result.text);return true elseif action==\"remote_result_clear\"then state.remoteResult=nil;return true elseif action:sub(1,7)==\"rename:\"then auth.require(\"operator\")local path,name=action:sub(8),prompt(\"New name\")if not confirm(\"Rename \"..fs.getName(path)..\" to \"..name)then return\"Cancelled\"end local target=files.rename(path,name);state.selected=\"file:\"..target;return target elseif action:sub(1,6)==\"trash:\"then auth.require(\"operator\")local path=action:sub(7)if not confirm(\"Move \"..fs.getName(path)..\" to Trash\")then return\"Cancelled\"end local id=files.moveToTrash(path);state.selected=nil;return\"Moved to Trash\"elseif action:sub(1,8)==\"restore:\"then auth.require(\"operator\")local path=files.restore(action:sub(9));return\"Restored \"..fs.getName(path)elseif action:sub(1,13)==\"delete_trash:\"then auth.require(\"operator\")if not confirm(\"Delete this Trash item permanently\")then return\"Cancelled\"end files.deleteTrash(action:sub(14));return true elseif action==\"new_folder\"then auth.require(\"operator\")local name=prompt(\"Folder name\")if not confirm(\"Create folder \"..name)then return\"Cancelled\"end return files.makeDirectory(state.path,name)elseif action==\"empty_trash\"then auth.require(\"operator\")if not confirm(\"Permanently delete all Trash items\")then return\"Cancelled\"end files.emptyTrash();return true elseif action==\"start_discovery\"then local phrase=prompt(\"Choose discovery phrase\")local mode=prompt(\"Mode (peer/master)\")local master=mode==\"master\"and prompt(\"Master side (initiator/joiner)\")or\"initiator\"if not confirm(\"Start \"..(mode==\"master\"and\"master/slave\"or\"peer\")..\" discovery with this phrase\")then return\"Cancelled\"end local ok,err=networkRequest(\"discovery_start\",{phrase=phrase,mode=mode,master=master});if ok then return\"Discovery active (waiting for join)\"end;return err elseif action==\"join_discovery\"then local phrase=prompt(\"Discovery phrase\",true);local ok,result=networkRequest(\"discovery_join\",{phrase=phrase})if not ok then return result end if type(result)==\"table\"and result.state==\"candidate\"then local candidate=result.candidate or{}local peerLabel=tostring(candidate.label or(\"Computer \"..tostring(candidate.id or\"?\")))local relationship=candidate.mode==\"master\"and\"master/slave\"or\"equal-peer\"if not confirm(\"Pair directly with \"..peerLabel..\" (\"..relationship..\")\")then return\"Pairing confirmation cancelled\"end local confirmed,confirmErr=networkRequest(\"discovery_confirm\",{})return confirmed and\"Pairing confirmation sent\"or confirmErr end return\"Searching for a matching discovery session\"elseif action==\"confirm_discovery\"then if not confirm(\"Confirm the selected direct pairing\")then return\"Cancelled\"end local ok,err=networkRequest(\"discovery_confirm\",{})return ok and\"Pairing confirmation sent\"or err elseif action==\"cancel_discovery\"then networkRequest(\"discovery_cancel\",{});return\"Discovery cancelled\"elseif action==\"discover\"then return net.discover()elseif action==\"pair_offer\"then local id,code=prompt(\"Peer ID\"),prompt(\"Pair code\",true);if not confirm(\"Send pairing offer\")then return\"Cancelled\"end;return net.offer(id,code)elseif action==\"pair_accept\"then local id,code=prompt(\"Peer ID\"),prompt(\"Pair code\",true);local peerRole=prompt(\"Role (viewer/operator/admin)\");local master=confirm(\"Is this peer the master\")if not confirm(\"Accept pairing\")then return\"Cancelled\"end;return net.accept(id,code,peerRole==\"\"and\"operator\"or peerRole,master)elseif action==\"remote_refresh\"then local live=store.read(\"/ceetos/data/network-status.lua\",{});local choices={}for _,peer in ipairs(live.peers or net.peers())do choices[#choices+1]={label=(peer.label or(\"Computer \"..peer.id))..\" (\"..peer.id..\")\",value=peer.id}end local peer=choose(\"Refresh peripherals from\",choices);if not peer then return false end return beginRemoteRequest(\"list\",peer)elseif action:sub(1,15)==\"peripheral_set:\"then local name,mode=action:match(\"^peripheral_set:(.-):(.+)$\")if mode~=\"peer\"and mode~=\"master\"then return\"Invalid sharing mode\"end local current for _,item in ipairs(peripherals.describe())do if item.name==name then current=item.mode end end if current==mode then peripherals.unshare(name);return\"Unshared \"..name end peripherals.share(name,mode);return\"Shared \"..name..(mode==\"master\"and\" (master only)\"or\" (peers)\")elseif action:sub(1,19)==\"peripheral_unshare:\"then local name=action:sub(20)peripherals.unshare(name);return\"Unshared \"..name elseif action==\"share_all:peer\"or action==\"share_all:master\"then local mode=action:sub(11)if not confirm(\"Share every detected peripheral\"..(mode==\"master\"and\" with master only\"or\" with peers\"))then return\"Cancelled\"end return\"Shared \"..peripherals.shareAll(mode)..\" peripherals\"elseif action==\"unshare_all\"then if not confirm(\"Stop sharing every peripheral\")then return\"Cancelled\"end return\"Unshared \"..peripherals.unshareAll()..\" peripherals\"elseif action==\"remote_cache\"then local live,choices=store.read(\"/ceetos/data/network-status.lua\",{}),{}for _,peer in ipairs(live.peers or net.peers())do choices[#choices+1]={label=(peer.label or(\"Computer \"..peer.id))..\" (\"..peer.id..\")\",value=peer.id}end local peer=choose(\"View cached peripherals from\",choices);if not peer then return false end local cache=cacheFor(peer)state.remoteResult={status=\"ready\",peer=tostring(peer),text=tostring(#(cache.peripherals or{}))..\" cached peripherals\"}return true elseif action==\"remote_call\"then local live,peers=store.read(\"/ceetos/data/network-status.lua\",{}),{}for _,peer in ipairs(live.peers or net.peers())do peers[#peers+1]={label=(peer.label or(\"Computer \"..peer.id))..\" (\"..peer.id..\")\",value=peer}end local peer=choose(\"Choose peer\",peers);if not peer then return false end local cache=cacheFor(peer.id)if not cache.peripherals or#cache.peripherals==0 then return beginRemoteRequest(\"list\",peer.id,nil,true)end return chooseRemoteCall(peer,cache)elseif action==\"update_check\"then auth.require(\"operator\")local checked,err=networkRequest(\"update_check\",{})return checked and\"Checking for signed releases...\"or err elseif action:sub(1,16)==\"update_download:\"then auth.require(\"operator\")local source=action:sub(17)local started,err=networkRequest(\"update_download\",{source=source})return started and\"Verified download started\"or err elseif action==\"update_cancel\"then auth.require(\"operator\")local cancelled,err=networkRequest(\"update_cancel\",{})return cancelled and\"Update download cancelled\"or err elseif action==\"update_clear\"then auth.require(\"operator\")if not confirm(\"Clear the verified release cache\")then return\"Cancelled\"end local cleared,err=networkRequest(\"update_clear_cache\",{})return cleared and\"Release cache cleared\"or err elseif action==\"update_apply\"then auth.require(\"operator\")local statusOk,update=networkRequest(\"update_status\",{})if not statusOk or not update.cache then return(statusOk and\"No verified release is cached\"or update)end local label=\"Apply CeetOS \"..tostring(update.cache.version)..\" (\"..tostring(update.cache.fingerprint)..\", key \"..tostring(update.cache.keyId)..\")\"if not confirm(label)then return\"Cancelled\"end local prepared,err=networkRequest(\"update_apply\",{})if not prepared then return err end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)print(\"Verified release approved. Rebooting locally...\")sleep(0.3);os.reboot()elseif action==\"telemetry_toggle\"then if not confirm(\"Change telemetry state\")then return\"Cancelled\"end;telemetry.setEnabled(not telemetry.status());audit.log(auth.current.name,\"telemetry\",{enabled=telemetry.status()});return true elseif action==\"dashboard_configure\"then local url,poll,token=prompt(\"Telemetry URL\"),prompt(\"Poll URL\"),prompt(\"Dashboard token\",true);local terminal=confirm(\"Enable terminal polling\")if not confirm(\"Save dashboard configuration\")then return\"Cancelled\"end;telemetry.configure(url,poll,token,terminal);audit.log(auth.current.name,\"dashboard.configure\",{terminal=terminal});return true elseif action==\"cloud_enroll\"then auth.require(\"admin\")local code=prompt(\"One-time Cloud enrollment code\",true)if not confirm(\"Enroll this computer with auth.ceet.uk\")then return\"Cancelled\"end local enrolled,enrollErr=cloudSync.enroll(code)if not enrolled then return enrollErr end audit.log(auth.current.name,\"cloud.enroll\",{node=enrolled.node})return\"Cloud enrollment saved; syncing in the background\"elseif action==\"cloud_retry\"then auth.require(\"operator\")local synced,syncErr=cloudSync.tick(\"manual\")return synced and\"Cloud sync requested\"or syncErr elseif action==\"cloud_clear\"then auth.require(\"admin\")if not confirm(\"Remove this computer's Cloud enrollment\")then return\"Cancelled\"end local cleared,clearErr=cloudSync.clear()if cleared then audit.log(auth.current.name,\"cloud.clear\",{})end return cleared and\"Cloud enrollment removed\"or clearErr elseif action==\"login\"then return auth.login(prompt(\"User\"),prompt(\"Password\",true))elseif action==\"logout\"then if not confirm(\"Logout\")then return\"Cancelled\"end;auth.logout();return true elseif action==\"user_add\"then local name,userRole,password=prompt(\"User name\"),chooseRole(),prompt(\"Password\",true)if not userRole then return\"Cancelled\"end if not confirm(\"Create user \"..name)then return\"Cancelled\"end;auth.add(name,userRole,password);audit.log(auth.current.name,\"user.add\",{name=name,role=userRole});return true elseif action:sub(1,10)==\"user_role:\"then local name,userRole=action:match(\"^user_role:(.-):(.+)$\")if not confirm(\"Change \"..name..\" to \"..userRole)then return\"Cancelled\"end;auth.setRole(name,userRole);audit.log(auth.current.name,\"user.role\",{name=name,role=userRole});return true elseif action:sub(1,11)==\"user_reset:\"then local name,password=action:sub(12),prompt(\"New password for \"..action:sub(12),true)if not confirm(\"Reset password for \"..name)then return\"Cancelled\"end;auth.adminResetPassword(name,password);audit.log(auth.current.name,\"user.password\",{name=name});return true elseif action:sub(1,11)==\"user_delete:\"then local name=action:sub(12)if not confirm(\"Delete user \"..name)then return\"Cancelled\"end;auth.remove(name);audit.log(auth.current.name,\"user.delete\",{name=name});return true elseif action==\"recovery_enable\"then local minutes=tonumber(prompt(\"Recovery minutes (1-60)\"))or 10 if not confirm(\"Enable recovery window\")then return\"Cancelled\"end;store.write(\"/ceetos/data/recovery.lua\",{until_ts=os.epoch(\"utc\")+minutes*60000});audit.log(auth.current.name,\"recovery.enable\",{minutes=minutes});return true elseif action==\"recovery_reset\"then local recovery=store.read(\"/ceetos/data/recovery.lua\",{});assert((recovery.until_ts or 0)>os.epoch(\"utc\"),\"recovery window is not active\")local name,password=prompt(\"User name\"),prompt(\"New password\",true);if not confirm(\"Reset password for \"..name)then return\"Cancelled\"end;auth.resetPassword(name,password);audit.log(\"local-recovery\",\"password.reset\",{name=name});return true end end)if not ok then return refresh(tostring(result),true)end if result==false then return refresh(\"Request failed\",true)end return refresh()end local function activateSelected()local item=gui.selected(currentModel,state)if item and item.action then run(item.action)elseif item then refresh(\"This file cannot be opened\",true)end end local function nextEvent()while true do local event,first,second,third=os.pullEventRaw()if event~=\"terminate\"then return event,first,second,third end if auth.allowed(\"operator\")then if fs.exists(\"/ceetos/data\")then local marker=fs.open(\"/ceetos/data/desktop-stop\",\"w\")if marker then marker.write(\"terminate\");marker.close()end end return false end refresh(\"Viewer mode: Ctrl+T is disabled\",true)end end refresh()local uiTimer=os.startTimer(0.2)while true do local event,first,second,third=nextEvent()if event==false then return false end if event==\"timer\"and first==uiTimer then uiTimer=os.startTimer(0.2)if pollRemoteRequest()or page==\"updates\"then refresh()end elseif event==\"key\"then local name=keys.getName(first)if state.menu then if name==\"up\"then state.menu.selected=math.max(1,state.menu.selected-1);refresh()elseif name==\"down\"then state.menu.selected=math.min(#state.menu.items,state.menu.selected+1);refresh()elseif name==\"enter\"then run(state.menu.items[state.menu.selected].action)elseif name==\"backspace\"or name==\"m\"then state.menu=nil;refresh()end elseif name==\"q\"then if page==\"home\"then refresh(\"CeetOS Desktop stays active. Open Terminal for commands.\")else page=\"home\";state.selected=nil;refresh()end elseif name==\"r\"then refresh()elseif name==\"up\"or name==\"down\"or name==\"left\"or name==\"right\"then gui.move(currentModel,state,name);refresh()elseif name==\"enter\"then activateSelected()elseif name==\"m\"then openMenu(gui.selected(currentModel,state))elseif name==\"n\"and page==\"files\"and not state.trash then run(\"new_folder\")elseif name==\"t\"and page==\"files\"and not state.trash then run(\"trash_open\")elseif name==\"e\"and page==\"files\"and state.trash then run(\"empty_trash\")elseif name==\"backspace\"then if page==\"peripherals\"and state.peripheralView then state.peripheralView=false;state.selected=nil;refresh()elseif page==\"files\"and state.trash then state.trash=false;state.selected=nil;refresh()elseif page==\"files\"and state.path~=\"/\"then state.path=table.remove(state.history)or\"/\";state.selected=nil;refresh()else page=\"home\";state.selected=nil;refresh()end end elseif event==\"mouse_click\"then local target=terminalUi.hit(targets,second,third)if target then if target.kind==\"menu\"then run(target.item.action)elseif target.item.toggle then gui.select(currentModel,state,target.item);run(target.item.action)elseif first==2 then gui.select(currentModel,state,target.item);openMenu(target.item)elseif state.selected==target.item.id then if target.item.kind==\"account\"then openMenu(target.item)else activateSelected()end else gui.select(currentModel,state,target.item);refresh()end end elseif event==\"file_transfer\"then local ok,result=pcall(function()local transfers=first.getFiles();local imported,updated={},0 for _,transfer in ipairs(transfers)do local name=transfer.getName and transfer.getName()or\"\"local devBuild=name:match(\"^ceetos%-devbuild[%w%._%-]*%.lua$\")or name==\"ceetos-pastebin.lua\"local releaseBuild=name:match(\"^ceetos%-release[%w%._%-]*%.lua$\")or name==\"ceetos-release.lua\"if devBuild or releaseBuild then auth.require(\"operator\")local chunks={};while true do local chunk=transfer.read(8192);if not chunk then break end;chunks[#chunks+1]=chunk end local source=table.concat(chunks)if releaseBuild then local metadata,cacheErr=release.cache(source,\"file transfer\")assert(metadata,cacheErr or\"not a signed CeetOS release\")if confirm(\"Verified CeetOS \"..tostring(metadata.version)..\" received. Apply locally now\")then local prepared,applyErr=networkRequest(\"update_apply\",{})assert(prepared,applyErr or\"could not prepare update\")term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)print(\"Verified release approved. Rebooting locally...\")sleep(0.3);os.reboot()end updated=updated+1 else assert(source:find(\"CeetOS installer\",1,true)and source:find(\"local files\",1,true),\"not a CeetOS installer\")local installer=assert(load(source,name,\"t\"));installer();updated=updated+1 end else local importedFiles=files.import({transfer},\"/programs/imports\")for _,path in ipairs(importedFiles)do imported[#imported+1]=path end end end return{imported=imported,updated=updated}end)if ok then refresh()else refresh(tostring(result),true)end elseif event==\"term_resize\"then refresh()end end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)",
  ["ceetos/bin/ceetshell.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local history=require(\"ceetos.lib.terminal\")local clipboard=require(\"ceetos.lib.clipboard\")local inputState=require(\"ceetos.lib.input\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local auth=require(\"ceetos.lib.auth\")auth.require(\"operator\")local net=require(\"ceetos.lib.net\")local store=require(\"ceetos.lib.store\")local launch={...}local remoteId,remoteLabel,remoteSession if launch[1]==\"--remote\"and launch[2]then remoteId=tostring(launch[2]);remoteLabel=\"peer-\"..remoteId for _,peer in ipairs(net.peers())do if tostring(peer.id)==remoteId and peer.label and peer.label~=\"\"then remoteLabel=peer.label end end for _,peer in ipairs(net.seenPeers())do if tostring(peer.id)==remoteId and peer.label and peer.label~=\"\"then remoteLabel=peer.label end end remoteSession=tostring(os.epoch(\"utc\"))..\"-\"..tostring(math.random(1000,9999))end local function localLabel()return(os.getComputerLabel and os.getComputerLabel())or(\"computer-\"..tostring(os.getComputerID()))end local promptIdentity=(auth.current and auth.current.name or\"guest\")..\"@\"..(remoteLabel or localLabel())local function parse(line)local out,token,quote,escaped={},\"\",nil,false for index=1,#line do local char=line:sub(index,index)if escaped then token,escaped=token..char,false elseif char==\"\\\\\"then escaped=true elseif quote then if char==quote then quote=nil else token=token..char end elseif char=='\"'or char==\"'\"then quote=char elseif char:match(\"%s\")then if token~=\"\"then out[#out+1],token=token,\"\"end else token=token..char end end if token~=\"\"then out[#out+1]=token end return out end local function candidates(word)local result,seen={},{}local function add(value)if value and not seen[value]then result[#result+1],seen[value]=value,true end end if word:find(\"/\")then local directory,prefix=fs.getDir(word),fs.getName(word)directory=directory==\"\"and\"/\"or directory if fs.exists(directory)and fs.isDir(directory)then for _,name in ipairs(fs.list(directory))do if name:sub(1,#prefix)==prefix then add(fs.combine(directory,name))end end end else for directory in shell.path():gmatch(\"[^:]+\")do if fs.exists(directory)and fs.isDir(directory)then for _,name in ipairs(fs.list(directory))do add(name:gsub(\"%.lua$\",\"\"))end end end end return result end local function readCommand()local input,cursor,entries,index,draft,anchor,viewOffset=\"\",0,history.history(),nil,\"\",nil,0 local held=inputState.modifiers()local selection=require(\"ceetos.lib.selection\")local promptX,promptY=1,select(2,term.getCursorPos())local function draw()local width,height=term.getSize()local prompt=promptIdentity..\":~$ \"local available=math.max(1,width-#prompt)if cursor<viewOffset then viewOffset=cursor elseif cursor>viewOffset+available then viewOffset=cursor-available end viewOffset=math.max(0,math.min(math.max(0,#input-available),viewOffset))terminalUi.clearLine(promptY,colors.black);terminalUi.write(promptX,promptY,prompt,colors.lime,colors.black,width)local visible=input:sub(viewOffset+1,viewOffset+available)local lo,hi=anchor and math.min(anchor,cursor)or nil,anchor and math.max(anchor,cursor)or nil if lo and hi and lo~=hi then local from,to=math.max(lo,viewOffset),math.min(hi,viewOffset+#visible)if from<to then terminalUi.write(#prompt+1,promptY,input:sub(viewOffset+1,from),colors.white,colors.black,available)terminalUi.write(#prompt+1+from-viewOffset,promptY,input:sub(from+1,to),colors.black,colors.white,available-(from-viewOffset))terminalUi.write(#prompt+1+to-viewOffset,promptY,input:sub(to+1,viewOffset+#visible),colors.white,colors.black,available-(to-viewOffset))else terminalUi.write(#prompt+1,promptY,visible,colors.white,colors.black,available)end term.setBackgroundColor(colors.black)else terminalUi.write(#prompt+1,promptY,visible,colors.white,colors.black,available)end term.setCursorPos(math.min(width,#prompt+cursor-viewOffset+1),promptY);if term.setCursorBlink then term.setCursorBlink(true)end terminalUi.clearLine(height,colors.black);terminalUi.write(1,height,\"Cursor \"..tostring(cursor+1)..\"/\"..tostring(#input+1)..\"  |  Enter run  |  Tab complete  |  exit returns\",colors.gray,colors.black,width)term.setTextColor(colors.white);term.setCursorPos(math.min(width,#prompt+cursor-viewOffset+1),promptY)end draw()while true do local event,first,second,third=os.pullEvent()if event==\"char\"then if held.ctrl and held.shift and(first==\"c\"or first==\"C\")and anchor then local s=require(\"ceetos.lib.selection\").new({input});require(\"ceetos.lib.selection\").start(s,1,anchor);require(\"ceetos.lib.selection\").update(s,1,cursor);require(\"ceetos.lib.selection\").copy(s,clipboard)elseif held.ctrl and held.shift and(first==\"v\"or first==\"V\")then local text=clipboard.get();input=input:sub(1,cursor)..text..input:sub(cursor+1);cursor=cursor+#text;anchor=nil else if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil end;input=input:sub(1,cursor)..first..input:sub(cursor+1);cursor=cursor+#first end;draw()elseif event==\"paste\"then if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil end;input=input:sub(1,cursor)..first..input:sub(cursor+1);cursor=cursor+#first;draw()elseif event==\"key_down\"then inputState.key(held,keys.getName(first)or first)elseif event==\"key_up\"then inputState.keyUp(held,keys.getName(first)or first)elseif event==\"key\"then local key=tostring(keys.getName(first)or first or\"\"):lower()local copyShortcut=held.ctrl and held.shift and key==\"c\"local pasteShortcut=held.ctrl and held.shift and key==\"v\"if key==\"leftctrl\"or key==\"rightctrl\"or key==\"ctrl\"or key==\"leftshift\"or key==\"rightshift\"or key==\"shift\"then inputState.key(held,key);draw()elseif copyShortcut then held.ctrl,held.shift=false,false if anchor and anchor~=cursor then local s=selection.new({input});selection.start(s,1,anchor);selection.update(s,1,cursor);selection.copy(s,clipboard)end;draw()elseif pasteShortcut then held.ctrl,held.shift=false,false local text=clipboard.get();if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil end;input=input:sub(1,cursor)..text..input:sub(cursor+1);cursor=cursor+#text;draw()elseif key==\"enter\"then term.setCursorPos(1,promptY+1);return input elseif key==\"backspace\"then if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil elseif cursor>0 then input=input:sub(1,cursor-1)..input:sub(cursor+1);cursor=cursor-1 end;draw()elseif key==\"delete\"and cursor<#input then if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil else input=input:sub(1,cursor)..input:sub(cursor+2)end;draw()elseif key==\"left\"then if held.shift then anchor=anchor or cursor end;cursor=math.max(0,cursor-1);if not held.shift then anchor=nil end;draw()elseif key==\"right\"then if held.shift then anchor=anchor or cursor end;cursor=math.min(#input,cursor+1);if not held.shift then anchor=nil end;draw()elseif key==\"home\"then if held.shift then anchor=anchor or cursor end;cursor=0;if not held.shift then anchor=nil end;draw()elseif key==\"end\"then if held.shift then anchor=anchor or cursor end;cursor=#input;if not held.shift then anchor=nil end;draw()elseif key==\"up\"then if#entries>0 then if not index then draft,index=input,#entries else index=math.max(1,index-1)end;input,cursor=entries[index],#entries[index];draw()end elseif key==\"down\"and index then index=index+1;input=index>#entries and draft or entries[index];if index>#entries then index=nil end;cursor=#input;draw()elseif key==\"tab\"then local word=input:match(\"([^%s]*)$\")or\"\";local matches=history.complete(word,candidates(word))if#matches==1 then input=input:sub(1,#input-#word)..matches[1];cursor=#input;draw()end end elseif event==\"mouse_click\"and first==2 and anchor and anchor~=cursor then local s=selection.new({input});selection.start(s,1,anchor);selection.update(s,1,cursor);selection.copy(s,clipboard);draw()elseif event==\"mouse_click\"and first==1 and third==promptY then local prompt=promptIdentity..\":~$ \";anchor=math.max(0,math.min(#input,viewOffset+second-#prompt-1));cursor=anchor;draw()elseif event==\"mouse_drag\"and first==1 and third==promptY then local prompt=promptIdentity..\":~$ \";cursor=math.max(0,math.min(#input,viewOffset+second-#prompt-1));draw()end end end local function banner()local width=select(1,term.getSize())term.setBackgroundColor(colors.black);term.setTextColor(colors.cyan);term.clear();terminalUi.write(1,1,\"CeetOS Terminal  |  \"..promptIdentity..\"  |  Tab complete  |  exit returns to Desktop\",colors.cyan,colors.black,width)term.setCursorPos(1,3)term.setTextColor(colors.white)end local function waitRemote()if not remoteSession then return end local timer,deadline=os.startTimer(0.2),os.epoch(\"utc\")+2000 while os.epoch(\"utc\")<deadline do local event,value=os.pullEvent()if event==\"timer\"and value==timer then local replies,pending=store.read(\"/ceetos/data/shell-replies.lua\",{}),{}for _,reply in ipairs(replies)do if reply.from==remoteId and reply.body and reply.body.session==remoteSession then if reply.body.output and reply.body.output~=\"\"then print(reply.body.output)end if not reply.body.ok and reply.body.error then printError(reply.body.error)end else pending[#pending+1]=reply end end store.write(\"/ceetos/data/shell-replies.lua\",pending)if#pending<#replies then return end timer=os.startTimer(0.2)end end printError(\"Remote command timed out\")end local function configureDevBridge(url,token)if not url or url==\"\"then printError(\"usage: ceetdev <ws://bridge:port/dev> [token]\");return end if not fs.exists(\"/ceetos-dev\")then fs.makeDir(\"/ceetos-dev\")end local handle=assert(fs.open(\"/ceetos-dev/connection.lua\",\"w\"))handle.write(textutils.serialize({url=url,token=token or\"\",auth=(token and token~=\"\")and\"token\"or\"none\"}));handle.close()print(\"Development bridge configured; connection continues in the background.\")end banner()while true do local line=readCommand()if line==nil or line==\"exit\"then break end if line==\"clear\"then banner()else local args=parse(line)if#args>0 then history.add(line)if remoteId then local ok,err=net.request(remoteId,\"shell_request\",{command=line,session=remoteSession})if not ok then printError(err)else waitRemote()end else local ok,result if args[1]==\"ceetdev\"then configureDevBridge(args[2],args[3]);ok,result=true,true else ok,result=pcall(shell.run,args[1],table.unpack(args,2))end if not ok then printError(result)elseif result==false then printError(\"Command failed\")end end end end end",
  ["ceetos/bin/ceetlaunch.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local capabilities=require(\"ceetos.lib.program_capabilities\")local audit=require(\"ceetos.lib.audit\")local args={...}local requestedPath=table.remove(args,1)assert(requestedPath and requestedPath~=\"\",\"usage: ceetlaunch <program> [args]\")if auth.refreshSession then auth.refreshSession()end local current=auth.current local actor=current and current.name or\"anonymous\"local normalized,pathErr=capabilities.normaliseProgramPath(requestedPath)if not normalized then audit.log(actor,\"program.launch_denied\",{path=tostring(requestedPath),reason=tostring(pathErr)})error(pathErr,0)end local runner if auth.allowed(\"operator\")then audit.log(actor,\"program.launch\",{path=normalized,mode=\"full\",role=auth.current and auth.current.role})runner=function()return shell.run(normalized,table.unpack(args))end elseif auth.allowed(\"viewer\")then local chunk,record=capabilities.loadViewer(normalized,actor)if not chunk then audit.log(actor,\"program.launch_denied\",{path=normalized,reason=tostring(record)})error(record,0)end audit.log(actor,\"program.launch\",{path=normalized,mode=\"sandbox\",trust=record.trust,stale=record.stale==true,})runner=function()return chunk(table.unpack(args))end else audit.log(actor,\"program.launch_denied\",{path=normalized,reason=\"requires viewer\"})error(\"permission denied (requires viewer)\",0)end local child=coroutine.create(runner)local function resume(...)local ok,result=coroutine.resume(child,...)if not ok then audit.log(actor,\"program.failed\",{path=normalized,reason=\"program error\"})error(result,0)end return coroutine.status(child)==\"dead\",result end local done,result=resume()while not done do local event={os.pullEventRaw()}if event[1]==\"key\"and event[2]==keys.backspace then term.setTextColor(colors.gray)print(\"Backspace: returned to CeetOS Desktop\")return true end done,result=resume(table.unpack(event))end return result",
  ["ceetos/bin/craftos-shell.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")auth.require(\"operator\")shell.run(\"/rom/programs/shell.lua\")",
  ["ceetos/bin/ceetedit.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local editor=require(\"ceetos.lib.editor\")local clipboard=require(\"ceetos.lib.clipboard\")local inputState=require(\"ceetos.lib.input\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local auth=require(\"ceetos.lib.auth\")local files=require(\"ceetos.lib.files\")local args={...}local path=args[1]assert(path and path~=\"\",\"usage: ceetedit <file>\")auth.require(\"operator\")assert(not files.isProtected(path),\"protected files must be edited with craftos-edit\")local buffer=editor.new(fs.exists(path)and files.readText(path)or\"\")local scroll,ctrl,shift=1,false,false local held=inputState.modifiers()local tokenColours={text=colors.white,comment=colors.gray,string=colors.orange,keyword=colors.purple,number=colors.cyan,builtin=colors.lightBlue}local function fitCursor(height)local visible=math.max(1,height-3)if buffer.line<scroll then scroll=buffer.line elseif buffer.line>=scroll+visible then scroll=buffer.line-visible+1 end end local function render(message)local width,height=term.getSize();fitCursor(height)term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if width<24 or height<8 then terminalUi.minimum(\"CeetOS Editor\",\"Editor needs 24x8\",\"Resize | Ctrl+Q exit\");return end terminalUi.clearLine(1,colors.blue);terminalUi.write(2,1,\"CeetOS Editor | \"..path..(buffer.changed and\" *\"or\"\"),colors.white,colors.blue,width-2)local digits=math.max(2,#tostring(#buffer.lines));local gutter=digits+3 local lua=path:sub(-4):lower()==\".lua\"for row=2,height-1 do local lineNumber,line=scroll+row-2,buffer.lines[scroll+row-2]if line then local selected=lineNumber==buffer.line term.setBackgroundColor(selected and colors.gray or colors.black);terminalUi.write(1,row,(\"%\"..digits..\"d \"):format(lineNumber),colors.lightGray,selected and colors.gray or colors.black,gutter-1)term.setTextColor(colors.white)local x=gutter for _,token in ipairs(editor.tokens(line,lua))do if x<width then terminalUi.write(x,row,token.text,tokenColours[token.kind]or colors.white,selected and colors.gray or colors.black,width-x+1);x=x+#token.text end end end end local range=buffer.selection and require(\"ceetos.lib.selection\").range(buffer.selection)if range then for line=range.first.line,range.last.line do local row=line-scroll+2 if row>=2 and row<height then local from=line==range.first.line and range.first.column or 0 local to=line==range.last.line and range.last.column or#(buffer.lines[line]or\"\")if to>from and gutter+from<=width then terminalUi.write(gutter+from,row,(buffer.lines[line]or\"\"):sub(from+1,to),colors.black,colors.white,width-gutter-from+1)term.setBackgroundColor(colors.black)end end end end terminalUi.clearLine(height,colors.black);terminalUi.write(1,height,message or\"Ctrl+S save | F2 save as | Ctrl+F find | Ctrl+G line | F5 run | Ctrl+Q exit\",colors.gray,colors.black,width)local cursorY=buffer.line-scroll+2 if cursorY>=2 and cursorY<height then term.setCursorPos(math.min(width,gutter+buffer.column),cursorY)end term.setTextColor(colors.white)end local function prompt(label)local _,height=term.getSize();term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.setCursorPos(1,height);term.clearLine();term.write(label..\": \");return read()end local function save(target)target=target or path local ok,err=pcall(files.writeText,target,editor.text(buffer))if ok then path,buffer.changed=target,false;return true else render(tostring(err));return false end end local function leaveChoice()if not buffer.changed then return\"discard\"end local function draw()render()local width,height=term.getSize();local boxWidth=math.max(20,math.min(32,width-2));local x,y=math.max(1,math.floor((width-boxWidth)/2)+1),math.max(2,math.floor((height-5)/2))terminalUi.fill(x,y,boxWidth,5,colors.black)terminalUi.write(x+2,y+1,\"Unsaved changes\",colors.white,colors.black,boxWidth-4)local compact=boxWidth<31 local saveText,discardText,cancelText=compact and\"[S]\"or\" Save \",compact and\"[D]\"or\" Discard \",compact and\"[C]\"or\" Cancel \"local saveX,discardX,cancelX=x+2,x+2+#saveText+2,x+2+#saveText+#discardText+4 terminalUi.write(saveX,y+3,saveText,colors.black,colors.green)terminalUi.write(discardX,y+3,discardText,colors.white,colors.red)terminalUi.write(cancelX,y+3,cancelText,colors.white,colors.gray)return{y=y+3,saveX=saveX,saveWidth=#saveText,discardX=discardX,discardWidth=#discardText,cancelX=cancelX,cancelWidth=#cancelText}end local modal=draw()while true do local event,first,second,third=os.pullEvent()if event==\"mouse_click\"and third==modal.y then if second>=modal.saveX and second<modal.saveX+modal.saveWidth then return\"save\"elseif second>=modal.discardX and second<modal.discardX+modal.discardWidth then return\"discard\"elseif second>=modal.cancelX and second<modal.cancelX+modal.cancelWidth then return\"cancel\"end elseif event==\"key\"then local key=keys.getName(first);if key==\"enter\"then return\"save\"elseif key==\"d\"then return\"discard\"elseif key==\"backspace\"then return\"cancel\"end end if event==\"term_resize\"then modal=draw()end end end render()while true do local event,first,second,third=os.pullEvent()if event==\"char\"then editor.insert(buffer,first);render()elseif event==\"paste\"then editor.insert(buffer,first);render()elseif event==\"key_up\"then local released=tostring(keys.getName(first)or first or\"\"):lower()inputState.keyUp(held,released);ctrl,shift=held.ctrl,held.shift elseif event==\"key\"then local key=tostring(keys.getName(first)or first or\"\"):lower()if key==\"leftctrl\"or key==\"rightctrl\"or key==\"ctrl\"or key==\"leftshift\"or key==\"rightshift\"or key==\"shift\"then inputState.key(held,key);ctrl,shift=held.ctrl,held.shift elseif ctrl and key==\"c\"then editor.copySelection(buffer,clipboard);ctrl,shift,held.ctrl,held.shift=false,false,false,false;render(\"Copied\")elseif ctrl and key==\"v\"then editor.insert(buffer,clipboard.get());ctrl,shift,held.ctrl,held.shift=false,false,false,false;render()elseif ctrl and key==\"s\"then save();render(\"Saved\")elseif ctrl and key==\"q\"then local choice=leaveChoice();if choice==\"save\"then if save()then break end elseif choice==\"discard\"then break else render()end end elseif ctrl and key==\"f\"then local needle=prompt(\"Find\");if not editor.find(buffer,needle)then render(\"Not found\")else render()end elseif ctrl and key==\"g\"then editor.gotoLine(buffer,prompt(\"Go to line\"));render()elseif key==\"f2\"then local target=prompt(\"Save as\");if target~=\"\"then save(target);render(\"Saved\")else render()end elseif key==\"f5\"then if not auth.allowed(\"operator\")then render(\"Operator role required to run programs\")elseif save()then term.clear();term.setCursorPos(1,1);shell.run(\"/ceetos/bin/ceetlaunch.lua\",path);render(\"Returned from program\")end elseif key==\"left\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,0,-1);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"right\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,0,1);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"up\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,-1,0);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"down\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,1,0);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"home\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.gotoLine(buffer,buffer.line,0);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"end\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.gotoLine(buffer,buffer.line,#editor.current(buffer));if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"pageup\"then editor.move(buffer,-10,0);render()elseif key==\"pagedown\"then editor.move(buffer,10,0);render()elseif key==\"enter\"then editor.insert(buffer,\"\\n\");render()elseif key==\"tab\"then editor.insert(buffer,\"  \");render()elseif key==\"backspace\"then if not editor.deleteSelection(buffer)then editor.backspace(buffer)end;render()elseif key==\"delete\"then if not editor.deleteSelection(buffer)then editor.delete(buffer)end;render()elseif event==\"mouse_click\"and first==2 then if buffer.selection and require(\"ceetos.lib.selection\").range(buffer.selection)then editor.copySelection(buffer,clipboard);render(\"Copied\")else editor.insert(buffer,clipboard.get());render()end elseif event==\"mouse_click\"and first==1 then local width,height=term.getSize()local digits=math.max(2,#tostring(#buffer.lines));local gutter=digits+3 local line=scroll+third-2 if third>=2 and third<height and line>=1 and line<=#buffer.lines then local column=math.max(0,math.min(#(buffer.lines[line]or\"\"),second-gutter))editor.gotoLine(buffer,line,column);editor.selectStart(buffer);render()end elseif event==\"mouse_drag\"and first==1 then local width,height=term.getSize()local digits=math.max(2,#tostring(#buffer.lines));local gutter=digits+3 local line=math.max(1,math.min(#buffer.lines,scroll+third-2))local column=math.max(0,math.min(#(buffer.lines[line]or\"\"),second-gutter))editor.selectTo(buffer,line,column);editor.gotoLine(buffer,line,column);render()elseif event==\"term_resize\"then render()end end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)",
  ["ceetos/bin/edit.lua"] = "local args={...}if#args==0 then shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")else shell.run(\"/ceetos/bin/ceetedit.lua\",args[1])end",
  ["ceetos/bin/programs.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\")",
  ["ceetos/bin/files.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")",
  ["ceetos/bin/list.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")",
  ["ceetos/bin/delete.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")",
  ["ceetos/lib/store.lua"] = "local M={}local cache={}local function internalPath(path)return type(path)~=\"string\"or path:sub(-4)==\".new\"or path:sub(-4)==\".bak\"end local function call(fn,...)local result={pcall(fn,...)}if not result[1]then return false,tostring(result[2])end if result[2]==false then return false,tostring(result[3]or\"operation failed\")end return true,result[2],result[3]end local function exists(path)local ok,value=call(fs.exists,path)return ok and value==true end local function remove(path)if not exists(path)then return true end local ok,err=call(fs.delete,path)return ok,err end local function ensureDirectory(path)local dir=fs.getDir(path)if dir==\"\"or exists(dir)then return true end local ok,err=call(fs.makeDir,dir)return ok,err end local function readRaw(path)local ok,handleOrErr=call(fs.open,path,\"r\")if not ok or not handleOrErr then return false,nil,tostring(handleOrErr or\"could not open file\")end local handle=handleOrErr local readOk,content=call(handle.readAll)local closeOk,closeErr=call(handle.close)if not readOk then return false,nil,content end if not closeOk then return false,nil,closeErr end return true,content end local function writeRaw(path,content)local ok,handleOrErr=call(fs.open,path,\"w\")if not ok or not handleOrErr then return false,tostring(handleOrErr or\"could not open file\")end local handle=handleOrErr local writeOk,writeErr=call(handle.write,content)local closeOk,closeErr=call(handle.close)if not writeOk then return false,writeErr end if not closeOk then return false,closeErr end return true end local function move(source,destination)local ok,err=call(fs.move,source,destination)return ok,err end local function decode(raw)local ok,value=pcall(textutils.unserialise,raw)if not ok or value==nil then return false,nil,\"invalid serialised value\"end return true,value end local function readValue(path)local ok,raw,err=readRaw(path)if not ok then return false,nil,err end return decode(raw)end local function recoverValue(path)local activeOk,activeValue=false,nil if exists(path)then activeOk,activeValue=readValue(path)end if activeOk then remove(path..\".new\")remove(path..\".bak\")return true,activeValue end local backupOk,backupValue=false,nil if exists(path..\".bak\")then backupOk,backupValue=readValue(path..\".bak\")end if backupOk then if exists(path)then remove(path)end if not exists(path)then move(path..\".bak\",path)end remove(path..\".new\")return true,backupValue end remove(path..\".new\")return false,nil end local function validLines(raw)for line in(raw..\"\\n\"):gmatch(\"(.-)\\n\")do if line~=\"\"then local ok,value=pcall(textutils.unserialise,line)if not ok or value==nil then return false,\"invalid appended value\"end end end return true end local function recoverAppend(path)if exists(path)then local activeOk,raw=readRaw(path)if activeOk then local valid=validLines(raw)if valid then remove(path..\".new\")remove(path..\".bak\")return true,raw end end end if exists(path..\".bak\")then local backupOk,raw=readRaw(path..\".bak\")if backupOk and validLines(raw)then if exists(path)then remove(path)end if not exists(path)then move(path..\".bak\",path)end remove(path..\".new\")return true,raw end end remove(path..\".new\")return false,\"no valid appended value\"end local function commitRaw(path,raw,validator)local dirOk,dirErr=ensureDirectory(path)if not dirOk then return false,dirErr end local temporary,backup=path..\".new\",path..\".bak\"local removeOk,removeErr=remove(temporary)if not removeOk then return false,removeErr end local writeOk,writeErr=writeRaw(temporary,raw)if not writeOk then remove(temporary);return false,writeErr end local checkOk,checkedRaw,checkErr=readRaw(temporary)if not checkOk then remove(temporary);return false,checkErr end local valid,validErr=validator(checkedRaw)if not valid then remove(temporary);return false,validErr or\"temporary validation failed\"end local backupRemoveOk,backupRemoveErr=remove(backup)if not backupRemoveOk then remove(temporary);return false,backupRemoveErr end if exists(path)then local backupOk,backupErr=move(path,backup)if not backupOk then remove(temporary);return false,backupErr end local activateOk,activateErr=move(temporary,path)if not activateOk then if not exists(path)then move(backup,path)end remove(temporary)return false,activateErr end else local activateOk,activateErr=move(temporary,path)if not activateOk then remove(temporary);return false,activateErr end end return true end function M.readCached(path,fallback)if internalPath(path)then return fallback end local entry=cache[path]if entry~=nil then return entry.value end return M.readFresh(path,fallback)end function M.readFresh(path,fallback)if internalPath(path)then return fallback end local ok,value=recoverValue(path)if not ok then value=fallback end cache[path]={value=value}return value end function M.read(path,fallback)return M.readCached(path,fallback)end function M.writeAtomic(path,value)if internalPath(path)then return false,\"cannot write store temporary or backup path\"end if value==nil then return false,\"cannot store nil\"end local serialisedOk,raw=pcall(textutils.serialise,value,{compact=true})if not serialisedOk or type(raw)~=\"string\"then return false,tostring(raw or\"could not serialise value\")end local valid=decode(raw)if not valid then return false,\"could not validate serialised value\"end recoverValue(path)local ok,err=commitRaw(path,raw,decode)if ok then cache[path]={value=value}end return ok,err end function M.write(path,value)return M.writeAtomic(path,value)end function M.append(path,value,limits)if internalPath(path)then return false,\"cannot append to store temporary or backup path\"end if value==nil then return false,\"cannot append nil\"end local serialisedOk,line=pcall(textutils.serialise,value,{compact=true})if not serialisedOk or type(line)~=\"string\"then return false,tostring(line or\"could not serialise value\")end local valid=validLines(line..\"\\n\")if not valid then return false,\"could not validate appended value\"end local recovered,current=recoverAppend(path)if not recovered then if exists(path)or exists(path..\".bak\")then return false,current end current=\"\"end local lines={}for existing in(current..\"\\n\"):gmatch(\"(.-)\\n\")do if existing~=\"\"then lines[#lines+1]=existing end end lines[#lines+1]=line local maxEntries=limits and tonumber(limits.maxEntries)local maxBytes=limits and tonumber(limits.maxBytes)if maxEntries then maxEntries=math.max(1,math.floor(maxEntries))end if maxBytes then maxBytes=math.max(1,math.floor(maxBytes))end while maxEntries and#lines>maxEntries do table.remove(lines,1)end local raw=table.concat(lines,\"\\n\")if#raw>0 then raw=raw..\"\\n\"end while maxBytes and#raw>maxBytes and#lines>1 do table.remove(lines,1)raw=table.concat(lines,\"\\n\")..\"\\n\"end if maxBytes and#raw>maxBytes then return false,\"appended value exceeds byte limit\"end local ok,err=commitRaw(path,raw,validLines)if ok then cache[path]=nil end return ok,err end function M.readAppend(path)if internalPath(path)then return\"\"end local ok,raw=recoverAppend(path)return ok and raw or\"\"end function M.invalidate(path)cache[path]=nil end return M",
  ["ceetos/lib/profile.lua"] = "local store=require(\"ceetos.lib.store\")local M={}M.PATH=\"/ceetos/profile.lua\"M.IDS={desktop=true,[\"recipe-server\"]=true,[\"router-server\"]=true,[\"auth-server\"]=true}local cache local function normalise(value)if type(value)~=\"table\"then value={}end local id=tostring(value.id or value.profile or\"desktop\")if not M.IDS[id]then id=\"desktop\"end return{schema=1,id=id,installedAt=tonumber(value.installedAt)or 0}end function M.current()if not cache then cache=normalise(store.read(M.PATH,{id=\"desktop\"}))end return cache end function M.id()return M.current().id end function M.is(id)return M.id()==id end function M.isServer()return M.id()~=\"desktop\"end function M.set(id)assert(M.IDS[id],\"invalid CeetOS profile\")cache={schema=1,id=id,installedAt=os.epoch(\"utc\")}return store.write(M.PATH,cache)end function M.capability(name)local id=M.id()local tableFor={desktop={desktop=true,craftingClient=true,orders=true},[\"recipe-server\"]={server=true,recipes=true,recipeImport=true,recipePlan=true},[\"router-server\"]={server=true,routing=true},[\"auth-server\"]={server=true,authAuthority=true},}return tableFor[id][name]==true end return M",
  ["ceetos/lib/auth.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local audit=require(\"ceetos.lib.audit\")local M={current=nil}local USERS=\"/ceetos/data/users.lua\"local SESSION=\"/ceetos/data/session.lua\"local TOMBSTONES=\"/ceetos/data/user-tombstones.lua\"local THROTTLE=\"/ceetos/data/login-throttle.lua\"local RECOVERY=\"/ceetos/data/recovery.lua\"local AUTHORITY=\"/ceetos/data/auth-authority.lua\"local CENTRAL_SESSION=\"/ceetos/data/auth-session.lua\"local DIRECTORY=\"/ceetos/data/auth-directory.lua\"local rank={viewer=1,operator=2,admin=3}M.SESSION_TTL_MS=8*60*60*1000 M.LOGIN_THROTTLE={failures=5,windowMs=5*60*1000,lockMs=60*1000,maxEntries=64}local configuredWorkFactor=password.DEFAULT_WORK_FACTOR local sessionPathDelete local function now()return os.epoch(\"utc\")end local function accountName(name)return type(name)==\"string\"and name:match(\"^[%w_%-]+$\")and#name<=32 end local function users()store.invalidate(USERS);return store.read(USERS,{})end local function save(value)return store.write(USERS,value)end local function tombstones()store.invalidate(TOMBSTONES);return store.read(TOMBSTONES,{})end local function saveTombstones(value)return store.write(TOMBSTONES,value)end local function authority()store.invalidate(AUTHORITY)local value=store.read(AUTHORITY,nil)return type(value)==\"table\"and value.active==true and type(value.id)==\"string\"and value or nil end function M.authority()return authority()end function M.centralActive()return authority()~=nil end local function centralDirectory()store.invalidate(DIRECTORY)local value=store.read(DIRECTORY,{})return type(value)==\"table\"and type(value.users)==\"table\"and value or{users={}}end function M.acceptAuthority(record)assert(type(record)==\"table\"and type(record.id)==\"string\"and#record.id<=64,\"invalid auth authority\")local public={schema=1,active=true,id=record.id,term=math.max(0,math.floor(tonumber(record.term)or 0)),revision=math.max(0,math.floor(tonumber(record.revision)or 0)),seen=now()}assert(store.write(AUTHORITY,public),\"could not save auth authority\")if type(record.directory)==\"table\"then assert(store.write(DIRECTORY,{schema=1,revision=public.revision,users=record.directory}),\"could not save account directory\")end local loaded,profile=pcall(require,\"ceetos.lib.profile\")if not loaded or not profile.is(\"auth-server\")then if fs.exists(USERS)then fs.delete(USERS)end if fs.exists(TOMBSTONES)then fs.delete(TOMBSTONES)end sessionPathDelete()end return public end function M.updateDirectory(directory,revision)if type(directory)~=\"table\"then return false,\"invalid account directory\"end return store.write(DIRECTORY,{schema=1,revision=math.max(0,math.floor(tonumber(revision)or 0)),users=directory})end function M.saveCentralSession(value)if type(value)~=\"table\"or not accountName(value.name)or type(value.role)~=\"string\"then return false,\"invalid central session\"end local session={sessionVersion=3,name=value.name,role=value.role,token=value.token,issued=value.issued,expires=value.expires,accountRevision=value.accountRevision,authority=value.authority}if type(session.token)~=\"string\"or#session.token<16 then return false,\"invalid central session token\"end store.write(CENTRAL_SESSION,session)M.current=nil return true end local function safeAudit(action,detail)pcall(audit.log,\"auth\",action,detail or{})end local function stamp(account)account.updated=now()account.origin=tostring(os.getComputerID())account.revision=math.max(0,math.floor(tonumber(account.revision)or 0))+1 return account end local function makeAccount(role,plainPassword,context)local record,err=password.record(plainPassword,password.newSalt(context),configuredWorkFactor)assert(record,err)record.role=role return stamp(record)end sessionPathDelete=function()if fs.exists(SESSION)then fs.delete(SESSION)end store.invalidate(SESSION)end local function saveSession()if M.current==nil then sessionPathDelete();return end local durable={sessionVersion=2,name=M.current.name,nonce=M.current.nonce,issued=M.current.issued,expires=M.current.expires,accountRevision=M.current.accountRevision,}store.write(SESSION,durable)end local function makeSession(name,account)local issued=now()M.current={name=name,role=account.role,nonce=password.newSalt(\"session:\"..name),issued=issued,expires=issued+M.SESSION_TTL_MS,accountRevision=tonumber(account.revision)or 0,}saveSession()return M.current end local function liveAccount(name)local all=users()return all[name],all end local function normaliseSession(saved,account)if type(saved)~=\"table\"or not account or account.disabled then return nil end if saved.sessionVersion==2 then local issued,expires,revision=tonumber(saved.issued),tonumber(saved.expires),tonumber(saved.accountRevision)if type(saved.nonce)~=\"string\"or#saved.nonce<16 or not issued or not expires or not revision then return nil end if expires<=now()or revision~=(tonumber(account.revision)or 0)then return nil end return{name=saved.name,role=account.role,nonce=saved.nonce,issued=issued,expires=expires,accountRevision=revision}end if saved.sessionVersion==nil and type(saved.role)==\"string\"then return\"legacy\"end return nil end function M.refreshSession()local central=authority()if central then store.invalidate(CENTRAL_SESSION)local saved=store.read(CENTRAL_SESSION,nil)local entry=type(saved)==\"table\"and centralDirectory().users[saved.name]or nil if type(saved)~=\"table\"or saved.sessionVersion~=3 or saved.authority~=central.id or type(saved.token)~=\"string\"or(tonumber(saved.expires)or 0)<=now()or type(entry)~=\"table\"or entry.disabled==true or saved.accountRevision~=entry.revision then M.current=nil if fs.exists(CENTRAL_SESSION)then fs.delete(CENTRAL_SESSION)end return nil end M.current={name=saved.name,role=entry.role,nonce=saved.token,issued=saved.issued,expires=saved.expires,accountRevision=saved.accountRevision,authority=central.id,central=true}return M.current end store.invalidate(SESSION)local saved=store.read(SESSION,nil)if type(saved)~=\"table\"or not accountName(saved.name)then M.current=nil;return nil end local account=liveAccount(saved.name)local session=normaliseSession(saved,account)if session==\"legacy\"then return makeSession(saved.name,account)end if not session then M.current=nil sessionPathDelete()return nil end M.current=session return M.current end local function throttleState()store.invalidate(THROTTLE)local data=store.read(THROTTLE,{})return type(data)==\"table\"and data or{}end local function saveThrottle(data)local entries={}for name,entry in pairs(data)do if accountName(name)and type(entry)==\"table\"and type(entry.last)==\"number\"then entries[#entries+1]={name=name,last=entry.last}end end table.sort(entries,function(a,b)return a.last>b.last end)local kept={}for index,entry in ipairs(entries)do if index<=M.LOGIN_THROTTLE.maxEntries then kept[entry.name]=data[entry.name]end end store.write(THROTTLE,kept)end local function locked(name)if not accountName(name)then return false end local entry=throttleState()[name]return entry and(tonumber(entry.lockedUntil)or 0)>now()or false end local function noteFailure(name,reason)if not accountName(name)then return end local data,entry,time=throttleState(),nil,now()entry=data[name]or{failures=0,first=time}if time-(tonumber(entry.first)or 0)>M.LOGIN_THROTTLE.windowMs then entry.failures,entry.first=0,time end entry.failures=math.max(0,math.floor(tonumber(entry.failures)or 0))+1 entry.last=time if entry.failures>=M.LOGIN_THROTTLE.failures then entry.lockedUntil=time+M.LOGIN_THROTTLE.lockMs end data[name]=entry saveThrottle(data)safeAudit(\"login.failure\",{account=name,reason=reason or\"invalid\",throttled=(entry.lockedUntil or 0)>time})end local function clearFailures(name)local data=throttleState()if data[name]~=nil then data[name]=nil;saveThrottle(data)end end function M.throttleStatus(name)local entry=accountName(name)and throttleState()[name]or nil if not entry then return{failures=0,lockedUntil=0}end return{failures=tonumber(entry.failures)or 0,lockedUntil=tonumber(entry.lockedUntil)or 0}end function M.setPasswordWorkFactor(value)local work,err=password.normaliseWorkFactor(value)assert(work,err)configuredWorkFactor=work return work end function M.passwordWorkFactor()return configuredWorkFactor end function M.bootstrap(initialPassword)if authority()then return false end local all=users()if next(all)then return false end local plainPassword=initialPassword if plainPassword==nil then term.write(\"Create CeetOS admin password: \");plainPassword=read(\"*\")end local account=makeAccount(\"admin\",plainPassword,\"admin\")all.admin=account assert(save(all),\"could not save administrator account\")makeSession(\"admin\",account)safeAudit(\"bootstrap\",{account=\"admin\"})return true end function M.login(name,plainPassword)if authority()then local client=require((\"ceetos.lib.auth_client\"))return client.login(name,plainPassword)end if not accountName(name)or type(plainPassword)~=\"string\"then noteFailure(name,\"invalid\")return false,\"invalid credentials\"end if locked(name)then safeAudit(\"login.failure\",{account=name,reason=\"throttled\",throttled=true})return false,\"too many failed attempts; try again later\"end local account,all=liveAccount(name)if not account or account.disabled then noteFailure(name,\"invalid\")return false,\"invalid credentials\"end local valid,state=password.verify(account,plainPassword)if not valid then noteFailure(name,\"invalid\")return false,\"invalid credentials\"end if state==\"legacy\"then local upgraded=makeAccount(account.role,plainPassword,\"upgrade:\"..name)upgraded.updated=account.updated or upgraded.updated upgraded.origin=account.origin or upgraded.origin upgraded.revision=math.max(tonumber(account.revision)or 0,upgraded.revision)stamp(upgraded)all[name]=upgraded assert(save(all),\"could not upgrade password record\")account=upgraded safeAudit(\"password.upgraded\",{account=name})end clearFailures(name)makeSession(name,account)safeAudit(\"login.success\",{account=name})return true end function M.logout()M.current=nil sessionPathDelete()if fs.exists(CENTRAL_SESSION)then fs.delete(CENTRAL_SESSION)end end function M.add(name,role,plainPassword)if authority()then assert(rank[role]and accountName(name),\"invalid user or role\")local record,err=makeAccount(role,plainPassword,\"central:\"..name)assert(record,err)return require((\"ceetos.lib.auth_client\")).mutate(\"add\",{name=name,role=role,record=record})end M.require(\"operator\")assert(rank[role],\"invalid role\")if role==\"admin\"then M.require(\"admin\")end assert(accountName(name),\"invalid user name\")local all=users()assert(not all[name],\"user exists\")all[name]=makeAccount(role,plainPassword,name)assert(save(all),\"could not save user\")safeAudit(\"user.add\",{account=name,role=role})end local function adminCount(all)local count=0 for _,account in pairs(all)do if account.role==\"admin\"and not account.disabled then count=count+1 end end return count end function M.setRole(name,role)if authority()then return require((\"ceetos.lib.auth_client\")).mutate(\"role\",{name=name,role=role})end M.require(\"operator\")assert(accountName(name)and rank[role],\"invalid user or role\")local all,account=users(),users()[name]assert(account,\"unknown user\")if role==\"admin\"or account.role==\"admin\"then M.require(\"admin\")end assert(not(M.current and M.current.name==name),\"cannot change the active account role\")assert(not(account.role==\"admin\"and role~=\"admin\"and adminCount(all)<=1),\"cannot remove the last administrator\")account.role=role stamp(account)all[name]=account assert(save(all),\"could not save account role\")safeAudit(\"user.role\",{account=name,role=role})end function M.setDisabled(name,disabled)if authority()then return require((\"ceetos.lib.auth_client\")).mutate(\"disable\",{name=name,disabled=disabled==true})end M.require(\"admin\")assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]assert(account,\"unknown user\")assert(not(M.current and M.current.name==name),\"cannot disable the active account\")if disabled then assert(not(account.role==\"admin\"and adminCount(all)<=1),\"cannot disable the last administrator\")end account.disabled=disabled==true stamp(account)all[name]=account assert(save(all),\"could not save account state\")safeAudit(\"user.disabled\",{account=name,disabled=account.disabled})end function M.remove(name)if authority()then return require((\"ceetos.lib.auth_client\")).mutate(\"remove\",{name=name})end M.require(\"operator\")assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]assert(account,\"unknown user\")if account.role==\"admin\"then M.require(\"admin\")end assert(not(M.current and M.current.name==name),\"cannot delete the active account\")assert(not(account.role==\"admin\"and adminCount(all)<=1),\"cannot delete the last administrator\")all[name]=nil local removed=tombstones()removed[name]=now()assert(saveTombstones(removed),\"could not save account tombstone\")assert(save(all),\"could not remove account\")safeAudit(\"user.remove\",{account=name})end local function recoveryActive()store.invalidate(RECOVERY)local recovery=store.read(RECOVERY,{})return type(recovery)==\"table\"and(tonumber(recovery.until_ts)or 0)>now()end local function setPassword(name,plainPassword)assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]assert(account,\"unknown user\")local replacement=makeAccount(account.role,plainPassword,\"reset:\"..name)replacement.updated=account.updated or replacement.updated replacement.origin=account.origin or replacement.origin replacement.revision=math.max(tonumber(account.revision)or 0,replacement.revision)replacement.disabled=account.disabled==true stamp(replacement)all[name]=replacement assert(save(all),\"could not save password\")if M.current and M.current.name==name then M.current=nil;sessionPathDelete()end safeAudit(\"password.reset\",{account=name})return true end function M.resetPassword(name,plainPassword)if authority()then local account=centralDirectory().users[name];assert(account,\"unknown user\")local record,err=makeAccount(account.role,plainPassword,\"central-reset:\"..name);assert(record,err)return require((\"ceetos.lib.auth_client\")).mutate(\"reset\",{name=name,record=record})end if not M.allowed(\"admin\")and not recoveryActive()then error(\"permission denied (requires admin or active recovery window)\",2)end return setPassword(name,plainPassword)end function M.adminResetPassword(name,plainPassword)if authority()then local account=centralDirectory().users[name];assert(account,\"unknown user\")local record,err=makeAccount(account.role,plainPassword,\"central-reset:\"..name);assert(record,err)return require((\"ceetos.lib.auth_client\")).mutate(\"reset\",{name=name,record=record})end M.require(\"operator\")local account=users()[name]assert(account,\"unknown user\")if account.role==\"admin\"then M.require(\"admin\")end return setPassword(name,plainPassword)end function M.allowed(role)if not rank[role]then return false end local current=M.refreshSession()return current~=nil and rank[current.role]>=rank[role]end function M.require(role)if not M.allowed(role)then error(\"permission denied (requires \"..tostring(role)..\")\",2)end end function M.list()local out={}local source=authority()and centralDirectory().users or users()for name,account in pairs(source)do out[#out+1]={name=name,role=account.role,disabled=account.disabled==true,revision=account.revision}end table.sort(out,function(a,b)return a.name<b.name end)return out end function M.snapshot()if authority()then return{users={},tombstones={},source=tostring(os.getComputerID()),schema=3,central=true}end return M.authoritySnapshot()end function M.authoritySnapshot()local copy={users={},tombstones={},source=tostring(os.getComputerID()),schema=2}for name,account in pairs(users())do copy.users[name]={role=account.role,salt=account.salt,verifier=account.verifier,passwordVersion=account.passwordVersion,scheme=account.scheme,workFactor=account.workFactor,revision=tonumber(account.revision)or 0,disabled=account.disabled==true,updated=account.updated or 0,origin=account.origin or copy.source,}end for name,removed in pairs(tombstones())do copy.tombstones[name]=removed end return copy end local function validIncomingPassword(account)if password.isModern(account)then local work=password.normaliseWorkFactor(account.workFactor)return work~=nil and type(account.verifier)==\"string\"and#account.verifier==64 and account.verifier:match(\"^[0-9a-f]+$\")~=nil end return type(account.salt)==\"string\"and#account.salt>0 and#account.salt<=256 and type(account.verifier)==\"string\"and account.verifier:match(\"^[0-9a-fA-F]+$\")~=nil and#account.verifier==8 end function M.merge(snapshot,authoritySeed)if authority()and authoritySeed~=true then return false,\"distributed account sync is disabled by central authority\"end if type(snapshot)~=\"table\"or type(snapshot.users)~=\"table\"then return false,\"invalid account sync\"end local all,removed,changed=users(),tombstones(),false for name,deletedAt in pairs(snapshot.tombstones or{})do if accountName(name)and type(deletedAt)==\"number\"and deletedAt>(removed[name]or 0)then local localAccount=all[name]if not localAccount or(localAccount.updated or 0)<=deletedAt then all[name],removed[name],changed=nil,deletedAt,true end end end for name,account in pairs(snapshot.users)do if accountName(name)and type(account)==\"table\"and rank[account.role]and validIncomingPassword(account)then local updated=tonumber(account.updated)or 0 local localAccount,incomingOrigin=all[name],tostring(account.origin or snapshot.source or\"\")local localOrigin=localAccount and tostring(localAccount.origin or os.getComputerID())or\"\"local wins=not localAccount or updated>(localAccount.updated or 0)or(updated==(localAccount.updated or 0)and incomingOrigin~=\"\"and incomingOrigin<localOrigin)if(removed[name]==nil or updated>removed[name])and wins then all[name]={role=account.role,salt=account.salt,verifier=account.verifier,passwordVersion=account.passwordVersion,scheme=account.scheme,workFactor=account.workFactor,revision=math.max(0,math.floor(tonumber(account.revision)or 0)),disabled=account.disabled==true,updated=updated,origin=incomingOrigin,}changed=true end end end if changed then assert(save(all),\"could not save account sync\")assert(saveTombstones(removed),\"could not save account tombstones\")M.refreshSession()end return changed end function M.authorityAccount(name)if not accountName(name)then return nil end return users()[name]end function M.authorityDirectory()local out={}for name,account in pairs(users())do out[name]={role=account.role,disabled=account.disabled==true,revision=tonumber(account.revision)or 0}end return out end function M.authorityMutate(action,fields)fields=type(fields)==\"table\"and fields or{}local name=fields.name assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]if action==\"add\"then assert(not account,\"user exists\")assert(rank[fields.role],\"invalid role\")local record=fields.record assert(type(record)==\"table\"and password.isModern(record),\"central account mutation requires a verifier record\")record.role,record.disabled=fields.role,false;all[name]=stamp(record);assert(save(all),\"could not save user\")elseif action==\"role\"then assert(account and rank[fields.role],\"unknown user or invalid role\")if account.role==\"admin\"and fields.role~=\"admin\"then local admins=0;for _,candidate in pairs(all)do if candidate.role==\"admin\"and not candidate.disabled then admins=admins+1 end end assert(admins>1,\"cannot demote the last active admin\")end account.role=fields.role;stamp(account);all[name]=account;assert(save(all),\"could not save role\")elseif action==\"disable\"then if account and account.role==\"admin\"and fields.disabled==true then local admins=0;for _,candidate in pairs(all)do if candidate.role==\"admin\"and not candidate.disabled then admins=admins+1 end end assert(admins>1,\"cannot disable the last active admin\")end assert(account,\"unknown user\");account.disabled=fields.disabled==true;stamp(account);all[name]=account;assert(save(all),\"could not save user state\")elseif action==\"remove\"then assert(account,\"unknown user\")if account.role==\"admin\"and not account.disabled then local admins=0;for _,candidate in pairs(all)do if candidate.role==\"admin\"and not candidate.disabled then admins=admins+1 end end assert(admins>1,\"cannot remove the last active admin\")end all[name]=nil;local removed=tombstones();removed[name]=now();assert(saveTombstones(removed),\"could not save tombstone\");assert(save(all),\"could not remove user\")elseif action==\"reset\"then assert(account,\"unknown user\")local replacement=fields.record assert(type(replacement)==\"table\"and password.isModern(replacement),\"central password reset requires a verifier record\")replacement.role,replacement.disabled=account.role,account.disabled==true;all[name]=stamp(replacement);assert(save(all),\"could not save password\")else error(\"unsupported account mutation\")end safeAudit(\"authority.\"..action,{account=name})return M.authorityDirectory()end M.refreshSession()return M",
  ["ceetos/lib/password.lua"] = "local M={}M.SCHEME=\"pbkdf2-hmac-sha256\"M.PASSWORD_VERSION=2 M.DEFAULT_WORK_FACTOR=256 M.MIN_WORK_FACTOR=64 M.MAX_WORK_FACTOR=2048 M.MIN_LENGTH=8 M.MAX_LENGTH=128 local MOD=4294967296 local band,bor,bxor,bnot=bit32.band,bit32.bor,bit32.bxor,bit32.bnot local rshift,lshift=bit32.rshift,bit32.lshift local unpack=table.unpack or unpack local K={0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,}local initial={0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19,}local function add(...)local value=0 for i=1,select(\"#\",...)do value=(value+(select(i,...)or 0))%MOD end return value end local function rrotate(value,amount)if bit32.rrotate then return bit32.rrotate(value,amount)end return bor(rshift(value,amount),lshift(value,32-amount))end local function word(bytes,index)return bytes:byte(index)*16777216+bytes:byte(index+1)*65536+bytes:byte(index+2)*256+bytes:byte(index+3)end local function packWord(value)return string.char(band(rshift(value,24),0xff),band(rshift(value,16),0xff),band(rshift(value,8),0xff),band(value,0xff))end local function sha256Raw(value)local bitLength=#value*8 value=value..string.char(0x80)value=value..string.rep(\"\\0\",(56-(#value%64))%64)value=value..packWord(math.floor(bitLength/MOD))..packWord(bitLength%MOD)local h={unpack(initial)}for offset=1,#value,64 do if#value>16384 and offset>1 and((offset-1)/64)%128==0 and type(sleep)==\"function\"then sleep(0)end local w={}for i=1,16 do w[i]=word(value,offset+(i-1)*4)end for i=17,64 do local x,y=w[i-15],w[i-2]local s0=bxor(rrotate(x,7),rrotate(x,18),rshift(x,3))local s1=bxor(rrotate(y,17),rrotate(y,19),rshift(y,10))w[i]=add(w[i-16],s0,w[i-7],s1)end local a,b,c,d,e,f,g,hh=unpack(h)for i=1,64 do local s1=bxor(rrotate(e,6),rrotate(e,11),rrotate(e,25))local choice=bxor(band(e,f),band(bnot(e),g))local t1=add(hh,s1,choice,K[i],w[i])local s0=bxor(rrotate(a,2),rrotate(a,13),rrotate(a,22))local majority=bxor(band(a,b),band(a,c),band(b,c))local t2=add(s0,majority)hh,g,f,e,d,c,b,a=g,f,e,add(d,t1),c,b,a,add(t1,t2)end h[1],h[2],h[3],h[4]=add(h[1],a),add(h[2],b),add(h[3],c),add(h[4],d)h[5],h[6],h[7],h[8]=add(h[5],e),add(h[6],f),add(h[7],g),add(h[8],hh)end local result={}for i=1,8 do result[i]=packWord(h[i])end return table.concat(result)end local function hmac(key,message)if#key>64 then key=sha256Raw(key)end key=key..string.rep(\"\\0\",64-#key)local inner,outer={},{}for i=1,64 do local byte=key:byte(i)inner[i],outer[i]=string.char(bxor(byte,0x36)),string.char(bxor(byte,0x5c))end return sha256Raw(table.concat(outer)..sha256Raw(table.concat(inner)..message))end local function xorBytes(left,right)local out={}for i=1,#left do out[i]=string.char(bxor(left:byte(i),right:byte(i)))end return table.concat(out)end local function cooperate(round)if round%32~=0 or not(os and os.queueEvent and os.pullEvent)then return end os.queueEvent(\"ceetos_password_yield\")os.pullEvent(\"ceetos_password_yield\")end local function toHex(value)return(value:gsub(\".\",function(byte)return string.format(\"%02x\",byte:byte())end))end local function constantEqual(left,right)if type(left)~=\"string\"or type(right)~=\"string\"then return false end local differing=bxor(#left,#right)local length=math.max(#left,#right)for i=1,length do differing=bor(differing,bxor(left:byte(i)or 0,right:byte(i)or 0))end return differing==0 end function M.hmacSha256(key,message)if type(key)~=\"string\"or type(message)~=\"string\"then return nil,\"HMAC key and message must be strings\"end return toHex(hmac(key,message))end function M.sha256(value)if type(value)~=\"string\"then return nil,\"SHA-256 input must be a string\"end return toHex(sha256Raw(value))end function M.constantTimeEqual(left,right)return constantEqual(left,right)end function M.legacyDigest(value)local h=2166136261 for i=1,#value do h=bxor(h,value:byte(i))h=(h*16777619)%MOD end return string.format(\"%08x\",h)end function M.normaliseWorkFactor(value)value=tonumber(value)if not value or value%1~=0 then return nil,\"invalid password work factor\"end if value<M.MIN_WORK_FACTOR or value>M.MAX_WORK_FACTOR then return nil,\"password work factor must be \"..M.MIN_WORK_FACTOR..\"-\"..M.MAX_WORK_FACTOR end return value end function M.policy(password)if type(password)~=\"string\"then return false,\"password is required\"end if#password<M.MIN_LENGTH or#password>M.MAX_LENGTH then return false,\"password must contain \"..M.MIN_LENGTH..\"-\"..M.MAX_LENGTH..\" characters\"end if password:find(\"[%z\\1-\\31\\127]\")then return false,\"password contains control characters\"end return true end function M.derive(password,salt,workFactor)local work,err=M.normaliseWorkFactor(workFactor)if not work then return nil,err end if type(password)~=\"string\"or type(salt)~=\"string\"or#salt<16 or#salt>256 then return nil,\"invalid password record\"end local block=string.char(0,0,0,1)local value=hmac(password,salt..block)local result=value for _=2,work do value=hmac(password,value)result=xorBytes(result,value)cooperate(_)end return toHex(result)end local saltCounter=0 function M.newSalt(context)saltCounter=saltCounter+1 local now=os.epoch and os.epoch(\"utc\")or 0 local computer=os.getComputerID and os.getComputerID()or 0 local label=os.getComputerLabel and os.getComputerLabel()or\"\"local random=math.random and math.random(0,2147483647)or 0 return toHex(sha256Raw(table.concat({tostring(now),tostring(computer),tostring(label),tostring(context or\"\"),tostring(saltCounter),tostring(random)},\":\"))):sub(1,32)end function M.record(password,salt,workFactor)local valid,err=M.policy(password)if not valid then return nil,err end local work,workErr=M.normaliseWorkFactor(workFactor or M.DEFAULT_WORK_FACTOR)if not work then return nil,workErr end salt=salt or M.newSalt()local verifier,deriveErr=M.derive(password,salt,work)if not verifier then return nil,deriveErr end return{passwordVersion=M.PASSWORD_VERSION,scheme=M.SCHEME,salt=salt,verifier=verifier,workFactor=work,}end function M.isModern(record)return type(record)==\"table\"and record.passwordVersion==M.PASSWORD_VERSION and record.scheme==M.SCHEME end function M.verify(record,password)if type(record)~=\"table\"or type(password)~=\"string\"or type(record.salt)~=\"string\"or type(record.verifier)~=\"string\"then return false,\"invalid password record\"end if M.isModern(record)then local verifier,err=M.derive(password,record.salt,record.workFactor)if not verifier then return false,err end return constantEqual(record.verifier,verifier),nil end return constantEqual(record.verifier,M.legacyDigest(record.salt..password)),\"legacy\"end return M",
  ["ceetos/lib/release_keys.lua"] = "return{algorithm=\"lamport-sha256-v1\",roots={[\"auth-server-release-0.16.1\"]={public=\"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\",profile=\"auth-server\"},[\"auth-server-release-0.16.2\"]={public=\"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\",profile=\"auth-server\"},[\"desktop-release-0.16.1\"]={public=\"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\",profile=\"desktop\"},[\"desktop-release-0.16.2\"]={public=\"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\",profile=\"desktop\"},[\"recipe-server-release-0.16.1\"]={public=\"JoT/ZvLACbMz17ZMmlTCoVU6XK/e88ifV7wnvr+Z0eBbz6LaPYBTViRg0RtxfAix1j51wjdF0HjURR0CvQB1FfzajaqnbI0oCJ9A3ya3fqxGoh/Mqb92j2ejgQidNYjc8NiXc+lbFRutmvJFvC3gmyOn/iYKwjTfDywt4sD+1KvI+idBnUI68Zn3VF0KxDgSC5nY1ZsN7RlbDkjnImsA5P2g360FS8l3a4UuFv1+6vpaFTIT7XNZwxu0lBvc9i9ABJHet6ykWfyl/Hr3OdMP+ALG+ly//OQ5dPHbhTZqdMnjZ4B25Scbw1/Gy2igCM5PbdMx/F62Ir1spj3nlrGwSgWuaScE094NWYHptQpXPj1eHCigcHTzQr1IJySM/HOs9Dxuj1mGL8JwAncrSoJmh3KfLTpnou1+OqOHNW7sr0i3yAk+I6e0Qr5R0g/ia/TCJ4LKaDHlJBsmrFAGSQbUu1hW1Org2pwN+1+AmoUthoj4MnaVzcDOxZfFW9LuKcczuSfUtbEIXbBCDVLVG8lvVI8QqWkh34CHl5g75M68QuoaJjduj9hCLxLpJRJ4h6/3jXcDZ5Si63ojj3hR/nFPli6a5IfUdh1NWwgSUqh8sQkyO8B4DXOx6eD8iTVA86mCs7eDuv2MHWCW6667lwUYrMOrVYApo3XRemQvBapIoe6zgFPpvG7/U2Mj1I3bRSE5lwihP55iDuUjRU7YDgH38iEJkxRe3Hs7grcarILdgVUWdu5DDTnBRcXTg1HnuTDSy+ueJ8bUf48l/4t/6ER6TguBypuUDo2SyRbynUoq2hBu2zz//FfmZ2RW0iLxDa0wcbqlHFYz909bpDX18FZZuMBNCYxTvWCUQSTWwcm8vf2Js1cdlGF65A0Kk8atKvlS6XM4Cj7iOhKJEibtR6NMBIE4P3+Jx5+6fIxuCNvJHDQKG6vLMhBiHYyGAnoq+DavDwA5cqb1Chsn4PRMqU494zlSWl6Qj3obglZMm96QNPxcD1klqsc0nlpNj9lmW7T4b775rLfhbrD+i7o9r60QIEW64esxWiF52UhgTu2NfpjEreu6OweHbI/5hTcZOobeM3hnNpnRqJU/VMJ+QwSWbeyfowitlEW7lKZHBSR9PPBSioLS7uyOopXTeklZfgnIDPFl1hswI2PsdspwSk1HEFikTlN22fvgbivk0KLhRHe38GYK9bX1QFTJfqFy9rS7V9GJNjguqG0HdSHaDkX4BjZg1KBsotSnDrkpNv2l9ra88tH9KoQTvnzgf+3DQVBfoMX4wnghaoqaqs71bqs7m9PNYUeVc0Kq6bC+pvOBhAL7NFxNKnhMIKNUhNj1FAR8Wxo/VF5r5bGPqrWDYAbNbQQWPZDFJw90lh7lSwsyHAO/WG88fw7jRNEgl4eAmADuCtsd30Af26PQmpj6Jom63pJL6pSi8nz1XfHty0+QVMNj5PBijkHKFDcbUEJQzOgpYh+aljEknqtBkepA6SQmZ5sfbQmE4MosS0gbVQ2PTJe/uUv8ZAjJWVf5NiQ7nG9WZ6ufuGi74+E18w2VlSc6M0KBLVZQjX+FyhnodjqgR4aa+TDMF0NRSE26oZOh1tWGFl11lDbNKUa4fmZma0wxm72v3nQkPs6yBnjolV/ZirGQ3QW3b6f46tmaBawGFBq+x75wtLEDRlHF9l4isDqlMejcxcNt97s6Hq16WNB1lYk6W6mrYJSZdr5+Fw3dlSLnYVC1BPEzAvgJQb5suZLpI8BysQt9OriMEJlkcNZA7nKbTNKqxxTaKQfayBHhbuObgXUxIeUKTfHq73/CE3z5LGxHG5vBTxW9QlN+tuuIEckNAIEOLNm3+UtJ+hOmCeiUKma9zSjEFrIOn4V0vZILnVXt0BPOniBEX2DBimgud+itd8HAHd6Fy7Y4XZnOPgXErbzxcWhfynzNGw6iaK/2a7cbkkiDKu+oK7DS/27kErxmF57amynYii/wzo0riEMNZaml641kHfbX7DRyXfmZct67h1PT71FdXBFf76x+VF8vOS8QKynzgbH6smW+wOSNH+el/HRpkeMR79IK+jUGGnYn7ZkCFojpwSYKgme5cCCm2V3RYJts2QAvaUdV9pH7u7W6U4kqj5x40KW9izhs77Gos6hVY9rzTXQyBJZWELcGLwXgXiXDfor1GUSGnBhh0MMZeQ9McFhMJsJJsy/hLSiGIfDgfHl7q/vBbocpaCqwO+OiXU1DL0HIEZAXRPJrD8zJiIg25yQVwE9joBoXAiRdwZEFHZTf2/hvAckpRZZ5ZlCRoLBIinfbheQbulLnfW0g0/+GAzh6PMZWV6F/uAn5nGXcAdKb3d98FuQjHOvAFvFJr0jSzWOxXHktAmf8xV7tTuOBEjcHeTyontBfnHSBUhh+viyfeT1pF1eigskZQk/coVnHB+dszaAERWYWzOVnN2FZToZU88CJZTNjKtvoHZZFnVVbdRPx0pAYsD8NwioaNpZo4PRiy76iyLxBPwIV9oJJYSgvZ3OaR3tdomzl9HxrFb200bO2luX8+i2qx3HkfUb+WOJbLH+U1KWqndjcCiG9UZsYQjXx4M/jTfExOLBwTQA0KoioKkExkhEHf4Jt4JOZuxbrAnp8sxiK1FbOOiwD+EZNKB3yvpXEBWB6jsfxM2rzLdIEuA2VdS9iDwbK/SQcaIAVFKS04SBzrTKLlCjG251EvlxcIn6tMs+ZDPysRsEfnxj7a+QQoyAsjzV891IrBrWYUH3SD/5vujV0M9S9LWJPEjV8pp5unwxPqJ3JYXNS5unrWOf4lNgykXcs7FQMD/OdyxnvCxdgZ+lAV0j3WCbYBetXI6Xu1FkRICQxP8cHmn3/d516D7n2PtlGy876t6+kDnx9CC7sVMJQnhOcIlRqwPfyeAztKUsKDh3YPflrKRosqzz+dec+PBUM5DPFZYxr2+m8iRXPBc6tH6DpMNkYxiwF8CynNrK28E9cEt/Y6d68Q9bXH9fVHibS2M2EUliUSZIOizC7oMHrJIW0xOsXw7XwecOJQUec7IV8ap5n+ZKH8n5lfqZ/GtUVENKuDqZ4KoqVuWpAEImZHOhTbFrkssEkBao5tCBA8z4yNaApB+Rac5NPNQ5IHlhFs8GeInzM55KdeTGIkDaPQBH45CRhmmPxe+lFO3AgrHAlg3XlsseAOf91gtBdHCOC4jFksPVYZyXhNv/7WAPa+NrQ2syil7c+9XsFq+mzogoqdKDJatw+1bbXKkPsS0gtgj1E+BbhwxMDmnxGYcoUx9+U9CWZde2LINwD7PQZ/RrPMrd9cbOEBeu+aRiXtBbWCemUV4q2pYESIJa7wE+rPOtih/SxEXwZetW6vQzCnRT5g0KP7BLVmWVR8J5zPlIW0ny9LfZUqwAmd/Cv11ffav5oHTJnSELIe48/rDbYUUzQ3reygQ9oD1V9eTqke0JTwvzx3f0s/4zWouRNqDuJsnRiDQD56Bk7+izNrbS870wW5wQcWBK2S8W2J3O5iBh0KYBlte0nG2DiJkagvBXpehL9AfkHCe7lr6UayLrdognID/fvfkiPeVCv9mYWpdmjB1gaKo3MPRq6X7h6GzmBnIK8xo6pNxommibvKNllbcw2bTb3IAZHIaSB1yhbEn/N5qlpmKHIamR1edJLf1eKWBQ1qdD/6AFxnpKBY0kps/4wTJwkDUEZrzxOxrwIIH/qsS43HBuA3tV3ZGIZgUW6n+EgK3wfi92/X1P2w3LhIauelw6dnTPnDdGHOG0UM75bu36T6BoRf0MgdXBRgBVSRtqRFVksTUYFjfftmQzXVu6dp3vgrLD7yxOFcWrpugGUrBgS20lE+Wys/bO9hZZz1bKVO4wIUQ18sd0HpofDfeEc/Z1sSZVziekjRqIGPumijgibKmGSvp5fMVqybngDuNvOS3P3zjtwm3CDMny6T/K2HgPTOViUCiAJhUOBmjqgEQ3SSHlFGU43myFSfjGv7UpO0eDjb0x73d/L/cIK8cFYBTK+HfwdsrBVSKoJdw6E1YbtbOLF5wixLmijNh1f3iocDOTS/ohPviy4K9HkPgR02e6dPZxtFrWgrOOq3sgAYJJh2Ko8WjWA5U02TQWdw/FWmF1fbJrGyG/wB1kSAkO909c7dGlprEIYdzsCmxfmuOxSDrAqf+0qk+lNVZ39KOPkH00IVXaC2oHptD+jcM1iGViOdaA7DtKtAiHqt4ahmHaPFi9K2XRUNwgv35W/+rQbaFY2/LMv75drC8C0df/IxOT31M4FDnLQo1s2M62GbuXz24RSmB/ZYr8Ek8+brfHB4sKOOmoJP/KE3qhu/Aa4Dx6DXL/9qQhtzkKLL92F3ORAAgGdVNZxw2QuTKVqP3x0Q86vWFdGaQH3A4O1AaQCptr2JgVY77sikr2KrLvGNnsimWmBjhHANZHv1Yh1BUycJCqRn/4aQft2gkGY5pyOjw/AYCrdul9YX1IOpR5ptFISEg/K4nvTECj/f2/n/mCASQncof7DRF+/9AS0l4EIM8dsFW/u/z81ShhSFkr4qYYqi5PwR7Ve0wJNjM35Kj8nqNT6XJv7EpQwT+n9O4SHGtblsANxV0MnP6zKQ1NAMHoN7iYNgJTqvamptGz9H0t4Ydx9frRURFE69EH4oJVMVnwHOrtXv3gMb+kyNsXmZHZvQTnfYbFKvRozgwapKXouw2W7K5I58b4PjiXSy1NR7eqParQInCP0E6EVxLrwNg2iHNoHuKlCLtUolNpYuFR1816Aoesjs46qZeBjQ0hjzgUHWaXRNYlvH8GtM8FW9vqS3CiE++TJENl4tmlyj+ims0iiGI1RhMFdKKzG9mSS3ddawE67zp7hjhxqvqo4hM2OaMKnDwUkDt2DpKuZJDiXwUfzlp8/TIU7RHruDs/lBAwTQKEPEkdvCUGCiPvQfk+l6hJuhOl5xRMaGsYvoIFVuD77uY2sk71r14n13cZEXy9tVssJT87/PZ0aLIFdEart/Ql8Zrxqf3FzfhaZoVyJMKI06+p0FKdmfXAeWgzfp4iDVbvnpZ/aor6m202Kez4T1qOl1twMA6TsAJjTCd6jKA0DoUbV1+I9oCSAvv9tbkSth+m9ZpBDD7FV6C4rIMRCsisdqa1iZ//oAuv1u2c2UVQWN4d9kHWboQnZkBbqBYdwSZ/WYivjQ0sBk09MwU7MRA5t/uMsiN6FJ6cxLwhoTONo/XlScZrrv74ul3QsbVJ680vs4YpidP34BoVV9ZExc4s+EGloX/mPUtrQA7EVzgmC4HOXo+cBbPJh7V+IYdquZxxBBQ7LG9s86Hifvm0KlIHqMbzJK4G2k//Zl6rv3cg4g1fIvHfRvlvzT6NiQWopET5QkhQehsfifaX+M01FCpqlRXr3lnJfLAfMW8rsXetGD9iAemmNxeOuE7SaBZe9bMHVOu/RGlg1TSejnn4x0LoSK6/oUiE8/w7h2xFaxztX2ZPYr4HFs06X7udPcyaeYqHh+OsHxm+W1Qokj1N2di6p9/AyIl54KDJGmyfOlzK3XnPZcAgl5BCQf7KAh9Bjf/bqXjtkb6R1ljC+LNOrLLbkDUS3OJLStB+nYUrCc8PqRJzaD9ZpB5ulFQO2RsmHznyfJ0GlFX/ntTkSzXP7W8rZ1solmd81hdkWRwli8TS6V+AJjqhjhh7YIN8R07twfB3zkwoFB9442k4irhCPF8+lczvJX5YFJ3ruVza4Om/iALGPyM8onSntK2e6z4xvFbTKU6k9elq6MEubf63C4lxOt7ODg26rA5tKrxmGoiriAMs4ggFYq/LgOnsmxfjXfSKuXNXIT/2NJH0kJhBhQqTo/yvBTlHhBQGSE9X6J2GxpDnw8ed/jX/G6udEW2xElaMLDENlKffIFtzqxKazHKnpDvmaEKOfrprgjKCAPCX0AHtcVr8l3y7xM/SwWiOxi/+jXvBGN6x5jhPQdWumQiCl3BKpsmc6T1soPqqxRjjXPyux28O7hEFl4IxhyI9NQ5Pvx9coA7bc4FR1nPcZb+e84Lgr5Ma7mMfZ2C+4dBdqLTt5Gb5WdC9KYhHFP6gWknLsavkoJ7j1LI6KZ9bsoR8QSLyhaupySMjpBCegJN99pYkKwMMVk7Ylu6d3vakjsgw7Tbxml943yVQTUgckprqn3eIp0JzcTbTYI8MtpnvleXMEdppANZvgZ2SksHwqV9qRqXZJDaOEUVULtI2Xdeh2C11i6YPUckde0tKx9LSwrktCt7pHsi4TWgnlxzJA5xpxxDVw64pdvRsJdWY5GNt5GgYqeQwSpEGoMl5gF+6MuU9PbLQF4tWeGHh8bMa7zoXfvSybSuKfwO6TZaXwhbd3dxOZae4pynAqQHHXBCosFSkofHpyjK/wDpyld95Jqjpju7Nn3cWSvQiPrtsSGDr20Egvxuz/g7yGdsSdWipwt6dgi/OsBkDGZEyU994zaDund/UAEkmNXkduldVgNKaRzerHJcxVMPqBPi0I4p3zbAAeQkWxUW7SVr34av1rC1uKpJ+j8Gtq9y83+nVt63HORSTQPfe5hAu+1HTvnCrgXWIazfT7ciLGtlolRi4yXQJdLeWlcN7XYuf6o6nkGuNwMG9r0TxRDbcmBHQfOODK5xo83YVK5SC8siCvwUeuZbtMcfzSFPs/WpouQHZX9ocT8XAp77/ecvd5VuMwMQ5ng5Vh8fFugKQNmXP8Jg2LKuitNeGlxeshrSAJ2uj4XW4SpzovkGNmyrzwotFi3fFXY3EJ1Ay1KaNgGvHSVifQZ8b57kRoGt008wc43r1PsEQgf9X6AE39/k7UKespUQh6H4LV4NcOxfgaXBSaSsbfjT4PmhvmLINOOieDvNGNs3Wx9b2hK4t+NpL4MhWjeQqoBcAb837VenvCOMd2j43yZkfXh8LQNye9vlHGvGLG+TZsKld/kL8Gc+7QZfF53iVWaoaNnT3fYVbABMmVjsMvo+7yDZc/Sh5D9SeCtMEPpfzAhuMGxSk1Xh3+iLTW9vN4cWXxTwnaaTtNzwkLPCbZ3EGnz+X4zk+at4hAf4dcr/xtzAdCS2qph8n/dkNO38hBIsN4/Mxj28Ft1aOFFbQo+2UC1LSpy9rOdEKucOrl1LcMXLl8IJO1cRAL5L9ucCpF5Tcrim2J6WG9LM+wAjUI0dklO1gDIoTNmK9jumSbiXaT0hTy9Qsivs1RkiNX1+nxafs5fYNtlnMFFlI/n9Bwsu+6z9nKUHlZLUKiGiXkLkOHuO/Ip7QdOAVnuvdVL0jPMHlhtvnpDCyoRdpFtv5INmyYe2SoXeHYLBiV/dgbn9NfhGK+w8fGIiYPuLQPtuFLBt2KEy5LsgwziElJc1K/egPkTT2hs6gNvATW9jTiLDKHVDTGhDNQNgGbVGtjVgOMEoD21UKiAEivQHp9tVp9iJnrBCPsRvZzroyPoTAwB7tY/eogJmWeAdCZxOTE3Wk6+r4xyv4ciTxpowoDxR3yIY1u/GBwQW+VtynOWnlsBSQjc+lP0iBNWtXnnQjr6T7238JaqNVvi6bmoBrso34jGQhzLa0fbUugUjCd8HbtIuQ4a0DIOsFg6oM4LeJFnHtf2+CKAyFN+vcmW/R3sdTNFoTnAdQhzpltqTjw9jbXPUCpkzv44XcJyS5cJZkkiz9XsUN4wtxN60wQ10Rtzl3bQ5F2CvoFJ4XuRdDAljL3CvI9ParJ3dDGIYbtTmF9Py2SR7KiCqSi1+NwIsmJEJ+0Mmn0QRC094dgSlcmwK9IDYpBHH5sVJRzuzLGN54BcsUfaUend+/Ad1/NorWUwFJxRAhxpKx5WtVyIXcaPbvZ14hWwFTjX+FTWGpgP70NPN9C9YSqp/37bqJ+BzvoXicycqEFuImls8O6CukSriS2a8riW73BxEF6NtLg3ASifurguEMCMKP7R0PgoN39SsWbd2QhHEeC4mSQ3OLX84lAXDpm7JIHj3oXnllsuorITep2CMDJD9cTJ5Rj7UdSyo+xGN0Trhkgz7XtcD2VwDkn4GSM74QpZxNPcjm9J72lnbEugDOTtunxnFOEPi7KhqAnnyPg19SmwXel6/4j1IU7AOHdEKCcFw9/Ah+s3jrGDSKuJSj8gG8doQn0JdzGJF15DKm5yB9DMuRK/dyeaFzqT9KghrbuOjgIKysoGQTDyx2LfPseJcV8UHVkupmKNJWn+CXKNn3zvH/v10iehxDaTNIM/vGJ+TzxxZaLUcmgO7klvPB9OftMeVeqbuuNs0ogjXt3/kcg3jIOSkwmQ+5IEfbgTaMekN4o1c5yJZcTwNtWM1Jday6Un5u/lYMZhk6pkcMfVXal+q71V+r+DF/XaTU68hn8xSAYwolFFTv/FLidXU1+/aIzwY/wErnlAEfGy1HNhSDB7Mn5p5mtIhrZJYjujmq/Lp0vNeo2T610arUk1DBCl6n2gWj9vvvyXbbOgNiGMg448JBkgaU/Schiuj8MzS36ycFbxj3RKOBIV7cVKXvgLnxh0Xcro3Hk8QbhIDg9lGHFRPnXp/SBgd15kM75mOLOSmt5YIvwnqZ2X5VKMPmkpMO+TQx2jgWTyrtPu51ooxqu4OBJNaeeSY2jWKnFZTWnBZBc8svqUu6kvhIA/w/JDZBCn8lXtD8ZENPRulHutC+cqgaiQgoqBdBvqYda96abWiEQApjjTkJPdr8vye7PBjywLJh52/RoQ/qEXrfzuq7TbiT6MB8qoPXVSehYVLHNNzCgF+l0J3UQhYUwfbl2eT2NYfNM3vbfCgm6TrdJFUGOBKOHssD/ehdfP+GSC9RnjFpiJg9POQ3tCxrbkWnEJFEjUBUeQK6Ek6lNBIEkGX2x8BKxIoADvUAmGpnFXyMItba0W7zcVQIc93nwXQ0xR4fe58Q17KmUDyUJ1sef0Uyr4cwyT8tKKgArIWPnL/yGbmKp6m5Kgpceiwt1qfysGe1EcvM/NDfPW0UZzqeeVpws/ABsWtbhr+9F9lqEl75dGRX6TGybcR3l0y2+vML+vF/17IDA3TkF0CBQPergkpm/k+BQU/oI9Gz0vuU31ola+lLzYug9n4HvMqdT39OPe+IUKni5zk50AC3NScapTaFZMhchBi6Up4iAIVRoA6NtwGEaqhV+BQTL/2nurJqZ/i0U2IyXrSV7WjRrw6SI/uYbu+UTzkSkS29Y8QaBghVm5Rp84ZsuRlW00hkvVyuBeApsyp7fcoI1N0cUG2FcSymslVfJpffHRtrZBrxANCOkl9dyZ9xhJ7BYqVRl0992GTHBkcKNeyLohRJVTeOA87F4TsXViVzSd+blLS7FvgUKGJM1nsghjnpFVfF8Sj8w2UoxgD3ee7GsPBFVfxajfkgS9PzSJslTKrFJJITmZYsnEZOY4emIEfmnbPRYXIMbxntfEzRQ0ayTjEHjQy7ncMhBw98AXEUNV9IX3gv1tCaqDU8wCA92QO2HG5V/Geeplmv5SU3lP2vxnHJCWWLtU7/nHTt77Qi/6XbrOis9fqSkFymFPrpqfZgrq6Xu4jLfa0ZA6sxVQK5ZSW1ApOJVV31wJYnR+gdhuRlybvwvrwMQ4ksJDqQNm2RfEwDdOH4b0Y8LIQ5QD//konpzuIUuSI04L2T7z8TxjWd/x5A+mhPac7JC+KZrh/Qd6IaSDQtoxC8pqudusobLzlU0tuMbNGAIhALT1bkg/8FU1Tl3qJCccfyssWvOXJ6R/4o4WdjP+a4Y9LRVOYOS1pFO17T7HT79eKJJ7s/TQwiOPsRhNBV4mXlQH0vVk4CD0j5bhsaIi0+vTDY3GTYkJPmEoOZWYKed51wt48EGuAAIpyiITCbygelYct9RybXdnVmX2nLAGaf5d/xPYAsbkeoO4hjldCxFHdm3rYEZ6aKpOmIRN8vczWdv4A8enp2kNi92R3FY37FAMDiBPQpKmklu/2PQtrthoa4Pgj57Gmg2Iw3VLOpxjnUuOh3sz7YYbNV6MomgBT94miIU23Z9iT853XzIC/0iFfxrKF6KJdXs7bDcYItJOV4UKnzIkSwd90U+4o55jyh5hCsoO3MzSnLWsBiGGZ9st1LIPCYWUQ4QBlfoQSw3KANdY/rRCgtNmFleM0G7b0hmJRItMFQqG7GSX2zbhBp210u+xxo+q9iNU4MgSZRTq53Dxvqon7xylCyNagh7xRy4yj141nGJHaoTIABDa0CWqP30nA606aHfzibe4VaSTNqhTdFM30F5iyjFn7oIK9h7xBOJaIf9hJtC7v0ya/7tYMkDPARHERN5VueZ6mkDvOKH8dbvszAdmis069sB7+P4y+FSHpohwT8T8nM7aOG6s+QzBj2J+3/S+8bCXkiCGjHuh4hNvnRj622UBuO9SXP8mtCx2rIY5/rI5u0wWv8w/acRdq+9W22xAtdhBJQvTN/Q8Q/fbW1H9FGYcZmyYcABB3oDIR5zZNBjg7HREyHviqdy7kPoq59MadlLbt7A5MNkEajJ8OPb2tFTF06mpAFk/3WW8e1F6gl60vCTSRndMiwC5un7/d5K0VzN4SxlfMCe5sZ2NAhWzSTxPRssAQILTv6lRFKymz2whmM6CZuNuaP5DwudpatjG3Zf6zU3poQh5xvuD+EQq8sJEl3N3yQRhRZagdQl1EDto7nS0qRPqJJeAMJ1mDDPwrdaehLSc8Wb74paB1v1W64kmAs0/LQini3RWu+FC023qJ2gyaMt8xvfayKrQkrdqtExbKwoemJNePcJoscLD4Lt+YXA/HgjB2B/VHSSlrYk2Q08tW6xcE/IDchutC/B6ezVRNG7q7CpceIIOystr+81KDsOYgFM76zTR2V7/rJfXJmEw8Qmy/na8P7KW++dvuoBP19cZS+HVepDWTfqKS1p+sv93ngaGtA7m3kPN3vTOOioCxl9gn45j3CciOuGYwHt24v1tpmkoBY1eoioTx/B+aCvhOlWxU6Vfr75BAuFSsNsoxcWIDpZRjushu0NFOACtyGfT8mO+tw4GKhxZ2gsBsyCpl7kUTbIp0ALAlQUgMzTwL7riwXwxshHFe+q5OMKG7f8LpUxUgDCrcwmuploU/XYvKAyBDg2s+7ceWMkD6XTy30rG1w9RYNjpJQ4K8gCJGW4vfehVbWwzzeIIrvGNxVKNOrxR+FdyYEFsLMEQm9GKkffuZHpxqdS07JwACeJj7QFkg1GH6zPERCdjXMob7d3dGL6uVj3TbKFuwa3Y5AoBhX5oAmkk90pBzFERI3fIiok6/5uSjBOIRP5IQ+q4WUvKVk8S0lUM2UR843bpeOl2L9oaQ/rPxGuHFO2vkww8ubRe+RIvsK6FyiJA3KRYUyDz+Nth3sYub4k6/8Qw+QC0iBZv1JzXy2Jb+o6N786WHO3jNabJylNjRWHS1omIYCy0O1MlHzRLSDrr5MgobRr5Dr0kV61uQ05oYIvHOvr+lWcu7QOstRuerXHLmrfx6cnNSZ35RGQivoEsX8HPgn2/TCuWP72bPxeBRlEoGyURSQYSea0qBOO4eJlVkpjv5l8zTUlbQQe0FM5N5NfLEjOYcpm+LnNBQEUmV/TNUmod932gPonlL4qnpic5iowR8880dls01a85YwvIvvmZWEQNiGGIcs9DY3frePqkrs3EH7XH2ay+bknqff4fDqRjNSwXtDKwYRVQ2bZBp41RY1kacSen66tVQHS88fBiAFUfh/gcJnggM2Gfux6FdYkddBPBMNtaD/wdUvb6PT8OSo8Ipbw31bC9ZmyHYfuSMTNY0YuOpxySLs+VGxzzkK6uQR46Yj9GyKWfAInz1DbP6SRJk0dwBRJK2NiQ3a75NVECch5UcBjU0mCUPpa8Ow2yAHp5jWWPgm55alkxusSiSasVBLBtOJUBN2gPqrow5AlpOQZASotOvQ8O5OkS/aZhDvouJatw2UuVm3ohyXOvwVF1kEEGKJD6k+cLviK24DiRndwl8+ms1d2GxqscrK++BE+ZNGjtI8ts/bVjj45VRg3Q3HyIn6FEKhJe2ymXEfRUs/+j/3dWBlSoEOZti3PE4pNeDvMnk4xOb/4rbpcssLe5Y8NgDCkQ8ahkzpHYUoRyvnlmHtpuQ9fdpZ1w8P9Ftkp+kKDAmFgqi+B5hLPHLoVP62zyMwO/6U/aseMA6B0YhgukaHgbgTe/U0Aaj+gCQV20n0aimJfWCEAGTqp6jyhJk4oAnJjB8teeNPyIyj6CmZYVOvya2+htVOjnjOxtJ6lPDu10ZUQiCYsvBlhYNUuhyfa85hKfM3YY7+MqsqXUgj91bMAIQ7QyzOcd84HFUuNuUoGxcVXHsSPkiAw1RCvcT+4ilAas+39SxOHzIV4ySiDfppLCOeSAA3qZenwfJxT6uF8CO1WonUWovckJGnr02jRErY3Il7Lck1Kcdr5Mo1nZApbZdA324SWnqH8z3FYaIubwrkD4fJlMtrld2sZ4q+jQhDZKKzf2IuEBHSEt6ZfzLzn+pDPbJUufal9zkJUCoS/PfmAsgNXxBrs05qLg8zoHyroRWy2ervopDXOlAQA9oquvC4PSdh0RR9uG/0l18HFva7yC5A2MsIy6VzwBbs06hIwJV7o8lCBggW2fAOoEOVwaQshG7jNSgUTeHs2OYHUBCRNVbV0YLWE5amH1EnxZ4cEmhqcn30n+euHnLyqYI9WiHKBT5QWjoTUWbxjjLCEPGkA/r75ZuNTqtr1HxVW30ZTCBZjzMpMr4/q+nwmpgNVdjV6Fb6Fz1KKirzx0s+GY4vVtoBNboOyG4AviD2Zvf9eGA52Sa9NlMIPgMb4QGjH1FWfIb2NYePh7n6L04VrW9LnlhlznFifLwcDzSz2wCXj+d656ENGDa41QsVFAkBTFb0l2Vo35dSe71TMSFDVFqI1uPvEsqsYRXOsKEdAQS+AFBxABD8nwUEPDV2+B3visyU5R3sCGMn+YCTy1huANOs9RMkxlw0mariku/SWi+EpigpJsnYU2f+vSSBUB3R11TLllWVUv7nlUjuUH4xNfywhni6dTvN9nL5XKr0Vu+4wYT5p550qjrUBkKa6IaMmdayH9c4sU+kiqw5VW511SvXRlxoFzED0VBmhH3pfS5tkGgJGFmOigE+q1K2yEfPT35yMiEcfYGceP37UPeXYQCUJszqSbnclQzwzWVN/E1RuRRMC39QNeYmt1ld2HokUWQAAl0xt2oBLKwqBijOxzeK/uR1U+0zhxRzq7Yab7LwCpYKaRiC9LQMc1hK+0YIwSqeCT4EWMqi6RzoAxMFa7CNuPQXsdta23yK5u308lfTZFBePzJzGLver0My7Ugwk2+e5iybdRpvRffnRpkqt/xWd3YcKMGKrpSzunnGl7F7a9vEX1VJl2Yzlt9epQZshW49DjLYWX+Jdq8scdQISHBvemtlNueZmid9Zl9YYJtkzOXoG0bLJUzIHQB9vJZcDrcTs69pkmgLJvff5apbKz31heXbeYEkctHWdAnIyvJ6CaNVpk50h9y7nPOiAj/8jgzdqjc2QDwBK5wHplp17BZ1okedxLLm/OoZ4Q08YGgeSN3YdxJcA3W/KF8kdSD3D1i4TXjOouG2cRGjfVn7gPbeMpgW4iBO8Wlwv8VVg/wQ3c0WI9IsTq3scjKiJoA+J/y0xSJIO2tABQDi9Zj19vr/TlLjWkz6XxOJYlDQorchzdfTwuUhoUtjIqHkm4BgFTgJP/jjJV8qb5u2het+cZeaiEXkNLpa8v57eLd3mhhYLUTjBWvq55nwiiZ8otwLearIocACtKSLzutK5OENZgQ2ndy5kh/5IIlrbKFstDePh7Pw8OQlMcx+eakpkO9+aN0Suu6NJ4CJZkpPaXPXcDsJhdbCg7hVi9v+BaV4aNOIJJ7jRsU3WPtgktxQvAWTZKJVYoaGSNSWv1xF+v61GogYWMC+7xJowm2xvo86J+i5rB9oQy6fOoWwZ1KxurBilhwqnvO7kHJUPZWU/+ISwRHYM+5cQrI0qz90IoKiSL0fuRqmfrzGQkQjPeeJx2bB6gjVy8qQ6HDm8dGlJGGPUgvbkbuq0+OiyxOhwGf1pRB6/A4LMeSQnR6ZwqLjo+ESa8idbpH0NENWzDggjQ4LE0tURdgZymMAcPXpaRPeniaEeXMpTyQIwZmG4xBTg1TUQqdaJA7HR4IbDj8DB1eKnr2i0z2ZQvtVM6YETYm7OL8s9seg9Eps31uS1apr4w8Zto3PzjcmjArgMYUg7fjnK9gRoBiB481tF+jFOEIDA8vWwzrEmI0URL1MTZ6PEOoZqBOa1eLJzQpZAzp9SemTRLBCHWFGEyBxG/T5ZeNfSTstm5NKE+X8klmOmmWyKWupcwf9PrC3KgTf5twjeSl0olayOO7abD+Q+flWymBIc0uaf4p3ciXMCFOeFqf6qcXckPnhQWAKmQM5wdM9pBp7eDfiIEYtrL0pbgFap1T9uaI6H7wCLpVdxkA1LPiJZ+xZOY/TsD6nF7bF94FS3epUv8qLPJBeuSYPc6Wf3zZFAmQuiClxcxz/3WiMmTsBGfBcR8sMmb6GFCqWEuTbEAmaICGqNyaihX3qf5a57rZu4BqVSe3YnhlB9b+S6RJC+/uFzDUDEx1BkwDO0/ZKG1q1xuV7urjrQFL5N0lnmKMxvFSoHYIM27lkTMxIv2gEJ2UctKN4QOBKLtQfo0yczMaMS7E9aSYek8TZf4Mo0QAI0zFiT86/tl4NbZvSN/7ViE7/YjIhotJaHd6vz1HOOb00HfqhhhbrqVqgHEFjsK2Hlbunk6OyX7Mh892r3pqEzoErw51L23UE8tvB3PipjPqgbPRHisXEyiqPTE1KGKdEknY5qUSQMOfSrsEhr8EKeQEEz1IZDJ51x50f9FtKsWKFZnEYfp5hELxYyw+JVgqYmSX+xPKWy061WmJUA83fb0qR65XQcnEIqifHEJuIiUwu/6fV2dGlvTxfDN1n2JZVGrHDl+8cvtbDptfMTbmfOi7I+ySfw+zdQOCro5nbIvTxJymPu2Ew5XG141oclsM9jjFOn5dPxYFjNoBv1kG6TIDmF5y+iMj6fObo5CZPxZ8flyNU2tczeVoVaCv2IbLOAjVYucryLnbJal/z7FsIBQafhL0QCJaKzMLs24hB8MFMzOBNyzVGOg/iaGv1ErG50laq5QNNMpdfHay2M0SdHDoca5ccB/P3bi1kcObkE6++OZoL4AX6cw05YytO863bpGz18/YE1Sit4Mb4EMUC3pstSOPDuh63Ob7Q1dF/p+E2XpTWjIKHzPAsoDmZiU+ifpbFcu+kpX12o92Uyjv3R+g5KVcC2M/IwcMydXhHJ8skb4rII26Q5spA2++STpSd+w5pZYJWfv+uyWW9apQNMBpUeCcwuaSUfu5+z4seCeRTdHzFX1AZHgqHeFLbBUhMYV76fwUQt/MoX+qbqUn2Zh9mFSm3ArWH8PVtX02L0CC9EHXrxWU5YcOjaIzckFECq3WWze1SotHytttucBQxCgqQHHxoqQDmPImAB4K4AtvySHwXP7LRNcDogPSERHEk+TOvj0qZ2vDdvyzH3KhnZJbg5Fk1GLQl9/jAN4Z7N81VDfju3KYnGxA+P7eTKiFyA7+0vlEK8z667ZvXaSecLg2rv7z//Uu3t/y8FdJ8KrChx5L5szv8HBUPJBQkCGhoqbQHadnwCyTbwQ65b+ascO+VEkTcJPX9X9arUC+LTPxd8RfHabCHc/hk57Ahqotvysbse3jX56mfdgw0Vi2ixXcOlo0oSWwdsQNQ9D9FgZRGnDr5SNkkOd/LYdff1Ypnj+1kx7okthVN+OPgl0i6+rEiyj2ecoFlhGt51ktmYNgDWaB/hcHH9Vyus1unIXmVr2edioA44E1UJlsxfciSxuUuNB2zk3ZBq5Fm+i9MOI/iQD3CiGcd4G9g6NdNsMEXMTTZ4UKhNk9BACt2oHbPcpAHJv0ytZa86p2Ns/LI6egYeNZ36vGOhxlLmGke9RqGTBHvx+KPneFgdOLsBsCUXid2F5RhZCzya+0eMTbCK6SNG4yHvI0KyAgRSCzbUoeFpxPWrZVoUJ4GjjsnhBRzNWSu4tIRQOkJz7/WxsvrqSyTI0oG2kKT6GYXAYcq9xpQE1J0xtoEM+oyuuxbKQ8zMnpBDrCYlR6bB3fGh+ms3+FNldYBot7uekFlc/ch7PuyUvpE4YE0BVoKhdkTvsLKXYWjrZWnHYVpU4HLyYSaNsjkmh2w4hj2GIeu0fVp2e/YDbV2OYlcgkAFHGR6Imp3RgIk0xFAKiGmpKFqusVLhVJxhnxvBBnNuBocckIpbV4QQ0VV/I6VkNDKp+p5FQ2ZtkK3OEW7A7p2dhjhRUCXC82vwBpth9JBs3qfV8Pw/LsA7043Md5CNNxaey0vxh2CZckJupAB8/IOQRYsI1UJ5H4KgkzaV3KRg4s5GE//ucVzvuaJnALyYXWMDM+OLzT994WHbv60kM570PRsX7yBHVK1B5+tgiR2HE6UdGhv/XQ2d4kaXF1iVgeXHb0KK3U4QwsLpwxqhart0Y9zYKbcIPRuuG1ZToQNi474jqySrC0sYMhlRakH0sN74I6t7t/aj+jga01s9tjG60G0W9aezgeH3cbv7Li3rpQTa12mgKrH9EjcUvd9Km7P07EbTpJ451Ix5oBwHu1Zv3ZRbSu8yKul3ztpxtgywO0SjmSA9S5S5S5TB1PR7l7ovIppDYcfPKWSl5kxd38KTcp0u8F3bBj7CbASwKBBZSFHGQAU/GPa3Nt8R5oXG9DkLeBun8zOdMvU0qzS0BraTLX8dmSTzqPubSnvSMmELBCIAoZ6BxXN7oYlucb16OSi5o0NaIw1TTF1rBlBHa9gwMNw7RJthpig4lt6BtxX3Mq5zONUnKmjtlOzY2Nje5nTJHOjCNSxUCRxo6pAT/wUjsV76D0qZNOfdcNZQz9ARe3ZW5r4FUu6A9Fms4NnYo5COHUnmJloWO2OmFo4mCw3Dp43/WbqDDgqGJnn7etu/d30Rm7XNRNjwxTuapA1ui8+2Bzhikzc0/gqv0Mq4gwh8tqsRVAx1VbAIhuevJZ8XGqSX+a2CMU+CrjLFJ5mJuwEwjBIhlu+KlvXaGgsxnnoQ18qlH8TC4rWlPSpLg7Ma1vM1bx1yBhfBAB/qA4vZEYfXwwt9z1jKCfFfp0fXFMAGE9P5uhg1LHvhCYrjSOgVn6psyaUA8d4rg4WLr3PrMg6Ec2MZMc3aVQyqLs/vJPAKdadatkFSXxuUs3o35ECmofOmJZTh/bSCSZZvFJCsL5fe2dNEf4CRUFZaz28X5OaE8FOhFnecJlouMIAp20XaAYeAjqUg6X6LCSDjl5iTzxynFd69Trok7QP4RavOdRJ2GnWAR2iv3wqo8cCAKTo/hnMzCqRSY2HJMbqUp+WPNuTIRwwGbYxB8sSnIoa3ZLtpfq9eQwN0TrUzfCeTLIyB22Rc41Ec+WnQZtA/zrefwNgCFEA/T+Vx/zKf0Gfu7cebIuX6I6nJg/OoEULg+ilbM+N1jsmeRR0WN2cySwB0ftyXbPFn0H7Hc1v2RqBehOIaw0dflZ0nnSQCthW//b57B3Z8GVAmGXRxYtuX003JShiQfoWvOoJxfY8/OA0SpSrtH5zcBKWLGw3uZNML1HWM+Gsm5S+EobwPVBPtMvB7MoYRyJ0QUQq1A9xIKBqg7H0cYJfuLE7t+RenRBoD2yWpv/xP3e4QZwM9SZtjWaxFN9PB5FTEQXLTJV4QBoUiOkABHQ/Rqp5Uh9sjpGCiZy/kQrUM3iAWx0cvRZYlytP5/5zL5FZVn2s2msOPj4xEq+j0ueaokJ3AjPedZCwglZv6RZBpKpk1RXImLv2sCzcT7ylyibjFaHw6hWIgqAXhA3XKWkyhBnNr/a5BUVi3T3UBm+2Z2ZxKTENj3tVHGARqDkNByvGCEj+HD/d2s3zADTX7O36XWQJbW7gLc6LXQtxXBCOJdjdfZnppviqXeFIqdV1Sdy6hX/6ReQiN8xgXi0gl1gcr4tWyRofn4OVNYtiyhdLoczwxHp658tdyKy63/t0cCqGRkm9j5s/A4g6Xhqw3U8PM3ZlWyKFyM1a94amFhv2oJSCwjylAB/r5Ve1VCEdbci5lhuJLhReOI8uro/6jVv3KsNIhpzJhk2qrKw72+os1IqmpXIjjQ5G0FF2B3+a6OuziaDDzN5aVknczBmzxLjq4McrnSBbp26M8Uwbc3oyUcPl7g8QFM8JWIWsOAcLGIU0irLfOoXv9p/4Tmpf+qzIkU7RHJmr5GQVKCp2yj3UKpOC8IzftjUpwiCjk2/Iqd93MHuVRntVoEWtsNx5YnglBxnKmntazZUPv9OMFJtdr4GaBaMbyjJD9yCW76mEQqAziH4g0SnB3yfk5o63P9Cdvm40oK1oL7r/6K1imZGuXUspq7lRCBUYPOpWSfcEh/4xGtsClGISwxtBS+1hNTbaxJ48mbofMrep/UztaaRJsU267U4ZkCtJNXTdoglq+8Ytsr4QOvAy7bZoRTa2WucMIEvjhg7l3yovKXwuNibu1mqoBM8NeGMX91w//Q233cSBVP0YgGWr8drPXr4Tj8+U1Gj18dILx9IKB8bZmDIuCqUnrrN6qg/Q3I4kIba1bI0DPoOG+EIluYnRtXBbDcxfpNZcU0EMHFBpnIy26yypsCf6fC7aIrw3W/2PGZuSvSasHFvK0CZVPAppO+hCxlf8abeqAJ+hLAQSj0GMg5Gm4MV629bID56cSL1SwKw5RbD936pRab4ipN9iZw0MOL7BHcBwCvKvDvfMuMNgG1WJL80ZPrZhsqGSnTuuUHtKJKbbDwd9TDG5D+5Zd4S80JksiBv219YiKj8C2yb8s7X3iUtaicuehiKJohR/F0W9pyX+D1C+alTjWGi44UcinKGhUy+pc10mh8bcyIphwFZZTHVC/tx0qf/NeRd0nAHUf5lEB0TW57uHV+Nnz/ZPCEA3PxtF/ltbh6/viBcghaQgy5Dgd2E74/RGFnlPxKUcJ4gCgmgCoXtsSWzDwSZB7X2fYVDa6vEBEmbYAJYsi6A5oc318ujQdKG0ugPier5/5zB3ZiR/Jb59AUKybglwUHW3Q7qh5IaCczeuWxNnLMBGyf6DGV9INpxuMHQaqME/YBKxRwk8yuDSj46e3vgwGivXE2K5diQBTvsYDL7Q1ka3cjZCHjvUgYE1SeAyijusffF9H7QkO3kosVxCWICwCpJDB4ysTNjbuDyfqUm0+tGAdmg77ksI1pXuM9AMHAHC+rJhY8S9/oQFMR1r3ZgL57J+pIGFmNv/1OHpAvxbbOB5i30G5Qah/E/Zk0nXi0XbkmAjgI9gyrZPxcDTNTuy1tWKlfBNty+z/lmVIbUtlD+hLZsGZ447eUp0EVxpAedx2h4NtYy3KygXUxifRGbql1agDCWZDEtiJDApVsem5oKQe7OYVNuM7pUCCv4PHRMcHj+KEb5cDj7bpMZmHm9ziw/udO4JV11my/cxjuVq0Y8Ie1yQgLqXPSSH9Sp3+wCOYVOFXh/b8T+JbweSdeAgd+/L4Ugs/jSVrRCafFtn1maIfv8qZlEiVuILLl1pkKoIbdeUOZvBnA63FVODq7FDiiqA8Oe8rKfvkmv24HUSH9TxAvj3QNtBFl5/j9Q+fZs5mAKjB2SsuTHv/qtOmAZbhxub9B7JHT54spuAKhET/QEmrZUz1yxPkpVWNlQ7F03pJNAaqXJJ+VZnbRpKSbVqkakALjsz07Vv5CRMJdHFt/Dbvyp3n0ZJaTkqkhc9t2qQzO7Anf2yT1wlA+du6gOZxpG37fOg5usvmEc0zpYmC4JT6iIZqr/AwJAaAEOu8/RsiYi42c5HnsJk08DyNiLPZpi2j5KUnoSltZCZNfb4cEwDCxxdI3Et/9UW57FyHQE7Doiw+ZKJhoDntGvROFMBrrw7LNQJITHKOeySREXUBFHoZ9kfKGtYYF22aQt6LGS+gy/BcGsBpz7nokDEuRUttmfQwSVWsMoZ72M7Yl5Wsf0y0ACMUfMNtxCBaYOyHITvL23sxZUf7cqJZI0WQQ5nrs+3UupKl+za3fgNOgaCuwkD6JqD5x7pBqrD1IAIccUNfd3S+vI5Lo94WGe0QzKnD+kVKDIKq/IA/CWkwKnzzRMgj5sSl0ipLA8ai+XlvELt1Ds3l0ppW1PQD3vNG3yfRlHkp2p3i27Tfsf2Cy3IWa0dfCxgN8Tt6ZBWlbHDB99XPL1j/818MLLN52CBS55+EtLVltkF6UQ3sqlvFS+wtSrDrPcqz8C17mF/Tts+fBoOuWRvKyegs1pzOyWZGRGauWXh6+aRJqnTKIGjMmfTbleo7hgi1nfR2y3L+Nm+Lmq1+Na6tB18x0671dAMUJ+m3gdY1zg5O+TAuBExvqTLclpsfJEczwMQWNFsbGDDMVghUOiYOlYjw6PLYiEOftC7aF92pa4da3hgV1gt3OleQrE2sGkImD3mFZYgleXHy1Kfk8ljfI9jMwQtCFZCPmA1GPxMuoDbfWL2X3geyOLqoT2MBwB0CMD9VB0/UNnBvWLSKT19DDq9qjq0XN+Zp1yUmW3LT5xXMvCSts6hRw98P6QOFocbZbkdG1Ai4mZO2jITUm/4hGJ+110E6wNcA/K5qNncd0erXav5ZL4HG/D2baTedeWhQfUCOmnKYkMIMSaFf8n61GzKjaUNRGeuXUjQq2wmy4dkVS1OfjiI6sLhaokj0iBOXpsiQTHak/Ih/SIs9QjaYSRZD5UxWyeZSQ8Z6SwpZJNvjtZZMjZsD3JwYCCfGJsTx3uAeSIVCVedzp6zouEe+cNGnDji7nPrXZZw26b6MQOQqpBe9XBFgPLsL749azh+6ITJDOJd43YLZmqmA0obn5O7pn7bLd0+itxiRZVv6hOXA793SeILg4JoiXTTazVzAjlkxojMBRXKC5WMd7HYH6MHSOEr2gMq5cXGf0yHCXur6NqLOd2sDoZTDUrqK+QZ2Fse8iRgdqsbhELem1L+d7l5r7Qb0sxf26BWi6rLGyjM+AjXw/ZTAniT30B59eGIfpdBa7YV1uUTFH5sSQ78olGN0kJbp+J+Xzf3PsXS3ZRSqkIHZgBMNfESlEbf9WLyLnGhCdE/MUGvq9M3d8f+z+AjRtsjUDdAs+232sNXwumIrHCptsJ0513z+N9cX7HSa8GMW7GwIqUYzzJTXKDduukglrBvNCfTJ3+ppu6AFDD0lRoCKNn36VNapAOWcpk7+QolyFh3ovRGtmFTFr4+aOhFrb6d/tkRjS+zxpw6uq3A6dE2ZBrconP2V3iWczBphrsjy4f/VmgrsDhKAvMnGmV09/f+hRuxwSZPTTBgzia5E6XXmZbuy0d4gei28sCWsUTAkeZK+6h0syhc2aX3a50R46wb/2neLaKzt4tBT656bXkOap+nT5FCLPNArAsjmYrA2+9OraSmjTeFz5PoCbhAocPQtw12up7nJ2CCZxTsQ3PmY8OjXagAHKk801+JVub1DhJpbygmC+T4A4zowuqCkfIT6eaeFL8/GIWxhfLX34zOuGaye/lacAWMsY/W/YHucjO7qFV16x1OAh4TZFuJsHa1UAZy/Dar8rMQRJuRJGKmq6KJoQAeO2Pw2wQj6GSyg/ysSeiTGm/aXpbaahsFWFXHoqp4o4nHtLj1hUYRbvuTBUM2VSKD56NIlyjr3OZ99jOmdEbFoMOYhak1hOB4MTcvgXeLCa7MT61JLWwGTOMRY03FGJMVgpMQnKsZgwRicjLGT04GIYaUf00erLmuprFcKfinM8h1oDl06x7M50QyLnN1Efnr66yzelBj8E4Pc8oZ8iumSazfcs1y5dVsh4nvAV4uPT+tBgaCqvhdhBrj1T6VX/pa8zeZ3X5qs/fHU/omGkHN2N1Eb+xCqUVdSLpstZNoLuBcJsyf3rg0GOlBDog==\",profile=\"recipe-server\"},[\"recipe-server-release-0.16.2\"]={public=\"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\",profile=\"recipe-server\"},[\"router-server-release-0.16.1\"]={public=\"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\",profile=\"router-server\"},[\"router-server-release-0.16.2\"]={public=\"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\",profile=\"router-server\"},},}",
  ["ceetos/lib/release_verify.lua"] = "local password=require(\"ceetos.lib.password\")local roots=require(\"ceetos.lib.release_keys\")local profile=require(\"ceetos.lib.profile\")local M={}M.CACHE_ROOT=\"/ceetos-updates\"M.CACHE_FILE=\"/ceetos-updates/release.lua\"M.META_FILE=\"/ceetos-updates/cache.lua\"M.PART_FILE=\"/ceetos-updates/release.part\"M.PENDING_FILE=\"/ceetos-updates/pending.lua\"M.MAX_PACKAGE_BYTES=1024*1024 M.MAX_MANIFEST_BYTES=48*1024 M.CHUNK_BYTES=3072 M.KEY_PARTS=256 local alphabet=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"local decodeMap={}for i=1,#alphabet do decodeMap[alphabet:sub(i,i)]=i-1 end local function read(path)if not fs.exists(path)or fs.isDir(path)then return nil end local handle=fs.open(path,\"r\")if not handle then return nil end local value=handle.readAll();handle.close()return value end local function write(path,value)local parent=fs.getDir(path)if parent~=\"\"and not fs.exists(parent)then fs.makeDir(parent)end local temporary=path..\".tmp\"if fs.exists(temporary)then fs.delete(temporary)end local handle=assert(fs.open(temporary,\"w\"),\"could not create temporary release state\")handle.write(value);handle.close()if fs.exists(path)then fs.delete(path)end fs.move(temporary,path)end local function encode(value)local out,index={},1 while index<=#value do local a,b,c=value:byte(index,index+2)local n=(a or 0)*65536+(b or 0)*256+(c or 0)out[#out+1]=alphabet:sub(math.floor(n/262144)%64+1,math.floor(n/262144)%64+1)out[#out+1]=alphabet:sub(math.floor(n/4096)%64+1,math.floor(n/4096)%64+1)out[#out+1]=b and alphabet:sub(math.floor(n/64)%64+1,math.floor(n/64)%64+1)or\"=\"out[#out+1]=c and alphabet:sub(n%64+1,n%64+1)or\"=\"index=index+3 end return table.concat(out)end local function decode(value)if type(value)~=\"string\"or#value%4~=0 or#value>M.MAX_MANIFEST_BYTES*2 then return nil,\"invalid base64\"end local out={}for index=1,#value,4 do local a,b,c,d=value:sub(index,index),value:sub(index+1,index+1),value:sub(index+2,index+2),value:sub(index+3,index+3)if not decodeMap[a]or not decodeMap[b]or(c~=\"=\"and not decodeMap[c])or(d~=\"=\"and not decodeMap[d])then return nil,\"invalid base64\"end if(c==\"=\"and d~=\"=\")or((c==\"=\"or d==\"=\")and index+3~=#value)then return nil,\"invalid base64 padding\"end local n=decodeMap[a]*262144+decodeMap[b]*4096+(decodeMap[c]or 0)*64+(decodeMap[d]or 0)out[#out+1]=string.char(math.floor(n/65536)%256)if c~=\"=\"then out[#out+1]=string.char(math.floor(n/256)%256)end if d~=\"=\"then out[#out+1]=string.char(n%256)end end return table.concat(out)end local function unhex(value)if type(value)~=\"string\"or#value%2~=0 or value:find(\"[^%x]\")then return nil end return(value:gsub(\"..\",function(pair)return string.char(tonumber(pair,16))end))end local function semver(value)if type(value)~=\"string\"then return nil end local major,minor,patch=value:match(\"^(%d+)%.(%d+)%.(%d+)$\")if not major then return nil end return tonumber(major),tonumber(minor),tonumber(patch)end function M.compareVersions(left,right)local a,b,c=semver(left);local x,y,z=semver(right)if not a or not x then return nil,\"invalid semantic version\"end if a~=x then return a<x and-1 or 1 end if b~=y then return b<y and-1 or 1 end if c~=z then return c<z and-1 or 1 end return 0 end local function canonical(manifest)if manifest._legacyProfile==true then return table.concat({\"ceetos-release-v1\",tostring(manifest.channel),tostring(manifest.version),tostring(manifest.size),tostring(manifest.digest),tostring(manifest.key_id),tostring(manifest.next_key_id),tostring(manifest.next_public),},\"|\")end return table.concat({\"ceetos-release-v2\",tostring(manifest.channel),tostring(manifest.profile),tostring(manifest.version),tostring(manifest.size),tostring(manifest.digest),tostring(manifest.key_id),tostring(manifest.next_key_id),tostring(manifest.next_public),},\"|\")end local function unpackKey(value)local raw,err=decode(value)if not raw or#raw~=M.KEY_PARTS*2*32 then return nil,err or\"invalid public key\"end local parts={}for index=1,M.KEY_PARTS*2 do parts[index]=raw:sub((index-1)*32+1,index*32)end return parts end function M.validPublicKey(value)return unpackKey(value)end local function lookupKey(keyId)local saved=M.trustedKeys()local value=saved[keyId]if value then return unpackKey(value)end value=roots.roots and roots.roots[keyId]if type(value)==\"table\"and type(value.public)==\"string\"then value=value.public end return unpackKey(value)end local function bitAt(digest,index)local byte=digest:byte(math.floor((index-1)/8)+1)return bit32.band(bit32.rshift(byte,7-((index-1)%8)),1)end local function decodeSignature(value)local raw,err=decode(value)if not raw or#raw~=M.KEY_PARTS*32 then return nil,err or\"invalid signature\"end local parts={}for index=1,M.KEY_PARTS do parts[index]=raw:sub((index-1)*32+1,index*32)end return parts end local function verifySignature(manifest,signature)if roots.algorithm~=\"lamport-sha256-v1\"then return false,\"unsupported release signing algorithm\"end local public,publicErr=lookupKey(manifest.key_id)local parts,signatureErr=decodeSignature(signature)if not public or not parts then local prefix=not public and\"invalid release public key: \"or\"invalid release signature: \"return false,prefix..tostring(publicErr or signatureErr or\"unknown release key\")end local digest=unhex(password.sha256(canonical(manifest))or\"\")if not digest then return false,\"could not hash release manifest\"end for index=1,M.KEY_PARTS do local expected=public[(index-1)*2+bitAt(digest,index)+1]local candidate=unhex(password.sha256(parts[index])or\"\")if not candidate or not password.constantTimeEqual(candidate,expected)then return false,\"invalid release signature\"end end return true end local function validateManifest(manifest)if type(manifest)~=\"table\"or manifest.channel~=\"release\"then return nil,\"only signed release packages are accepted\"end if manifest.profile==nil then manifest.profile,manifest._legacyProfile=\"desktop\",true end if type(manifest.profile)~=\"string\"or not profile.IDS[manifest.profile]then return nil,\"invalid release profile\"end if not semver(manifest.version)or type(manifest.size)~=\"number\"or manifest.size<1 or manifest.size>M.MAX_PACKAGE_BYTES then return nil,\"invalid release manifest size or version\"end if type(manifest.digest)~=\"string\"or#manifest.digest~=64 or manifest.digest:find(\"[^%x]\")then return nil,\"invalid release digest\"end if type(manifest.key_id)~=\"string\"or#manifest.key_id<1 or#manifest.key_id>64 then return nil,\"invalid release key id\"end if type(manifest.next_key_id)~=\"string\"or#manifest.next_key_id<1 or#manifest.next_key_id>64 then return nil,\"invalid next release key id\"end local nextKey=unpackKey(manifest.next_public)if not nextKey then return nil,\"invalid successor release key\"end return manifest end function M.parse(source)if type(source)~=\"string\"or#source>M.MAX_PACKAGE_BYTES then return nil,\"release package is too large\"end local encodedManifest,encodedSignature=source:match(\"^%-%- CEETOS_RELEASE_MANIFEST:([^\\r\\n]+)[\\r\\n]+%-%- CEETOS_RELEASE_SIGNATURE:([^\\r\\n]+)\")if not encodedManifest then encodedManifest,encodedSignature=source:match(\"^%-%- CeetOS signed release%. Generated; do not edit%.[\\r\\n]+%-%- CEETOS_RELEASE_MANIFEST:([^\\r\\n]+)[\\r\\n]+%-%- CEETOS_RELEASE_SIGNATURE:([^\\r\\n]+)\")end local first=source:find(\"%-%- CEETOS_RELEASE_PAYLOAD_BEGIN[\\r\\n]+\")if not encodedManifest or not encodedSignature or not first then return nil,\"not a signed CeetOS release package\"end local decoded,decodeErr=decode(encodedManifest)if not decoded then return nil,\"invalid release manifest encoding: \"..tostring(decodeErr)end local loader=load(\"return \"..decoded,\"=release-manifest\",\"t\",{})if not loader then return nil,\"invalid release manifest\"end local ok,manifest=pcall(loader)if not ok then return nil,\"invalid release manifest\"end local valid,manifestErr=validateManifest(manifest)if not valid then return nil,manifestErr end local start=source:find(\"\\n\",first)+1 local finish=start+valid.size-1 local payload=source:sub(start,finish)local suffix=source:sub(finish+1)if suffix~=\"\\n-- CEETOS_RELEASE_PAYLOAD_END\\n\"and suffix~=\"\\n-- CEETOS_RELEASE_PAYLOAD_END\"and suffix~=\"\\r\\n-- CEETOS_RELEASE_PAYLOAD_END\\r\\n\"and suffix~=\"\\r\\n-- CEETOS_RELEASE_PAYLOAD_END\"then return nil,\"release payload boundary mismatch\"end if#payload~=valid.size or not password.constantTimeEqual(password.sha256(payload)or\"\",valid.digest)then return nil,\"release payload digest mismatch\"end local signed,signatureErr=verifySignature(valid,encodedSignature)if not signed then return nil,signatureErr end return{manifest=valid,signature=encodedSignature,payload=payload,source=source}end function M.trustedKeys()local raw=read(M.CACHE_ROOT..\"/trusted-keys.lua\")if not raw then return{}end local loader=load(raw,\"=trusted-release-keys\",\"t\",{})if not loader then return{}end local ok,value=pcall(loader)return ok and type(value)==\"table\"and value or{}end local function saveKeys(value)local encoded=textutils.serialise(value,{compact=true})write(M.CACHE_ROOT..\"/trusted-keys.lua\",\"return \"..encoded)end function M.metadata()local raw=read(M.META_FILE)if not raw then return nil end local loader=load(raw,\"=release-cache\",\"t\",{})if not loader then return nil end local ok,value=pcall(loader)return ok and type(value)==\"table\"and value or nil end function M.cache(source,provider)local package,err=M.parse(source)if not package then return nil,err end if fs.exists(M.CACHE_ROOT)and fs.isDir(M.CACHE_ROOT)then if fs.exists(M.PART_FILE)then fs.delete(M.PART_FILE)end elseif not fs.exists(M.CACHE_ROOT)then fs.makeDir(M.CACHE_ROOT)end write(M.CACHE_FILE,package.source)local metadata={schema=2,channel=package.manifest.channel,profile=package.manifest.profile,version=package.manifest.version,size=package.manifest.size,digest=package.manifest.digest,keyId=package.manifest.key_id,nextKeyId=package.manifest.next_key_id,source=provider and tostring(provider)or\"local\",cachedAt=os.epoch(\"utc\"),}write(M.META_FILE,\"return \"..textutils.serialise(metadata,{compact=true}))local keys=M.trustedKeys();keys[package.manifest.next_key_id]=package.manifest.next_public;saveKeys(keys)return metadata end function M.promotePart(path,provider)if type(path)~=\"string\"or path~=M.PART_FILE then return nil,\"invalid release staging path\"end local source=read(path)local package,err=M.parse(source)if not package then return nil,err end if not fs.exists(M.CACHE_ROOT)then fs.makeDir(M.CACHE_ROOT)end if fs.exists(M.CACHE_FILE)then fs.delete(M.CACHE_FILE)end fs.move(M.PART_FILE,M.CACHE_FILE)local metadata={schema=2,channel=package.manifest.channel,profile=package.manifest.profile,version=package.manifest.version,size=package.manifest.size,digest=package.manifest.digest,keyId=package.manifest.key_id,nextKeyId=package.manifest.next_key_id,source=provider and tostring(provider)or\"local\",cachedAt=os.epoch(\"utc\"),}write(M.META_FILE,\"return \"..textutils.serialise(metadata,{compact=true}))M.adopt(package)return metadata end function M.adopt(package)if type(package)~=\"table\"or type(package.manifest)~=\"table\"then return false,\"invalid verified package\"end local keys=M.trustedKeys()keys[package.manifest.next_key_id]=package.manifest.next_public saveKeys(keys)return true end function M.cachedPackage()local source,metadata=read(M.CACHE_FILE),M.metadata()if not source then return nil end local package,err=M.parse(source)if not package then return nil,err end if not metadata then metadata={schema=2,channel=package.manifest.channel,profile=package.manifest.profile,version=package.manifest.version,size=package.manifest.size,digest=package.manifest.digest,keyId=package.manifest.key_id,nextKeyId=package.manifest.next_key_id,source=\"local installer\",cachedAt=os.epoch(\"utc\"),}write(M.META_FILE,\"return \"..textutils.serialise(metadata,{compact=true}))end M.adopt(package)return package,metadata end function M.clear()if fs.exists(M.PART_FILE)then fs.delete(M.PART_FILE)end if fs.exists(M.CACHE_FILE)then fs.delete(M.CACHE_FILE)end if fs.exists(M.META_FILE)then fs.delete(M.META_FILE)end return true end function M.encode(value)return encode(value)end function M.decode(value)return decode(value)end function M.canonical(manifest)return canonical(manifest)end return M",
  ["ceetos/lib/updater.lua"] = "local store=require(\"ceetos.lib.store\")local release=require(\"ceetos.lib.release_verify\")local version=require(\"ceetos.lib.version\")local net=require(\"ceetos.lib.net\")local profile=require(\"ceetos.lib.profile\")local M={}local STATE_PATH=\"/ceetos/data/update-status.lua\"local OFFER_LIMIT,HISTORY_LIMIT=20,16 local BEACON_INTERVAL,REQUEST_TIMEOUT,REQUEST_LIMIT=30000,3500,3 local MAX_OFFER_AGE=5*60*1000 local state=nil local lastBeacon,lastCacheRead=0,0 local cached=nil local function now()return os.epoch(\"utc\")end local function localId()return tostring(os.getComputerID())end local function localLabel()return os.getComputerLabel()or(\"Computer \"..localId())end local function defaults()return{schema=1,offers={},history={},revision=0,status=\"idle\",message=nil,transfer=nil,cache=nil}end local function loadState()if state then return state end state=store.read(STATE_PATH,defaults())for key,value in pairs(defaults())do if state[key]==nil then state[key]=value end end state.offers=type(state.offers)==\"table\"and state.offers or{}state.history=type(state.history)==\"table\"and state.history or{}return state end local function save()local value=loadState()value.revision=(tonumber(value.revision)or 0)+1 store.write(STATE_PATH,value)end local function record(kind,fields)local value=loadState()local row={at=now(),kind=tostring(kind):sub(1,48)}for key,item in pairs(fields or{})do if key~=\"data\"and key~=\"source\"and key~=\"signature\"then row[key]=item end end value.history[#value.history+1]=row while#value.history>HISTORY_LIMIT do table.remove(value.history,1)end end local function setStatus(status,message)local value=loadState()value.status,value.message=status,message and tostring(message):sub(1,180)or nil end local function validId(value)return type(value)==\"string\"and value:match(\"^[%w_%-%.]+$\")and#value<=96 end local function validOffer(body)if type(body)~=\"table\"or type(body.version)~=\"string\"or type(body.size)~=\"number\"or type(body.digest)~=\"string\"or type(body.keyId)~=\"string\"or type(body.profile)~=\"string\"then return nil,\"malformed release offer\"end if body.profile~=profile.id()then return nil,\"release profile does not match this installation\"end local compare=release.compareVersions(body.version,version.string)if compare==nil then return nil,\"invalid release version\"end if body.size<1 or body.size>release.MAX_PACKAGE_BYTES then return nil,\"invalid release size\"end if#body.digest~=64 or body.digest:find(\"[^%x]\")then return nil,\"invalid release digest\"end if#body.keyId<1 or#body.keyId>64 then return nil,\"invalid release key ID\"end return{profile=body.profile,version=body.version,size=math.floor(body.size),digest=body.digest:lower(),keyId=body.keyId,fingerprint=body.digest:sub(1,16),newer=compare>0}end local function refreshCached()local timestamp=now()if cached and timestamp-lastCacheRead<5000 then return cached end lastCacheRead=timestamp local value=loadState()local previous=value.cache and(tostring(value.cache.version)..\":\"..tostring(value.cache.digest))or nil local package,metadata=release.cachedPackage()if package and package.manifest.profile==profile.id()then cached={package=package,metadata=metadata,offer={version=package.manifest.version,size=#package.source,digest=package.manifest.digest,profile=package.manifest.profile,keyId=package.manifest.key_id,fingerprint=package.manifest.digest:sub(1,16),}}value.cache={profile=package.manifest.profile,version=package.manifest.version,size=#package.source,digest=package.manifest.digest,keyId=package.manifest.key_id,fingerprint=package.manifest.digest:sub(1,16),source=metadata and metadata.source or\"local\",verifiedAt=timestamp}else cached=nil value.cache=nil end local current=value.cache and(tostring(value.cache.version)..\":\"..tostring(value.cache.digest))or nil if current~=previous then save()end return cached end local function providerOffer()local available=refreshCached()if not available then return nil end return{version=available.offer.version,size=available.offer.size,digest=available.offer.digest,profile=available.offer.profile,keyId=available.offer.keyId,label=localLabel(),id=localId(),}end local function upsertOffer(source,body,transport,distance)source=tostring(source)if source==localId()then return false,\"ignored own release offer\"end local offer,err=validOffer(body)if not offer then return false,err end if not offer.newer then return false,\"offered release is not newer\"end local value=loadState()local key=source..\":\"..offer.version..\":\"..offer.digest:sub(1,16)value.offers[key]={id=source,label=tostring(body.label or(\"Computer \"..source)):sub(1,96),version=offer.version,profile=offer.profile,size=offer.size,digest=offer.digest,fingerprint=offer.fingerprint,keyId=offer.keyId,transport=transport,distance=tonumber(distance),seen=now(),key=key,}local rows={}for entryKey,item in pairs(value.offers)do rows[#rows+1]={key=entryKey,seen=tonumber(item.seen)or 0}end table.sort(rows,function(a,b)return a.seen>b.seen end)while#rows>OFFER_LIMIT do value.offers[table.remove(rows).key]=nil end setStatus(\"offers\",\"Signed release \"..offer.version..\" available from \"..tostring(body.label or source))record(\"offer\",{id=source,version=offer.version,transport=transport,distance=distance})save()return true end local function offers()local value,timestamp,rows=loadState(),now(),{}for key,item in pairs(value.offers)do if type(item)==\"table\"and timestamp-(tonumber(item.seen)or 0)<=MAX_OFFER_AGE then rows[#rows+1]=item else value.offers[key]=nil end end table.sort(rows,function(left,right)local compare=release.compareVersions(left.version,right.version)or 0 if compare~=0 then return compare>0 end return tostring(left.id)<tostring(right.id)end)return rows end local function findOffer(source)source=tostring(source)for _,item in ipairs(offers())do if item.key==source or tostring(item.id)==source then return item end end return nil,\"release source is unavailable; run update check first\"end local function removePart()if fs.exists(release.PART_FILE)then fs.delete(release.PART_FILE)end end local function appendPart(data)local handle,err=fs.open(release.PART_FILE,\"a\")if not handle then return false,err or\"could not open update staging file\"end handle.write(data);handle.close()return true end local function cachedSource()local available=refreshCached()return available and available.package and available.package.source end local function transferRequest(transfer)local length=math.min(release.CHUNK_BYTES,transfer.size-transfer.offset+1)if length<1 then return false,\"transfer is already complete\"end transfer.awaiting,transfer.requestedAt=true,now()local body={transfer=transfer.id,offset=transfer.offset,length=length}local sent,err if transfer.transport==\"routed\"then sent,err=net.request(transfer.source,\"update_chunk_request\",body,{safe=true})else sent,err=net.publicSend(transfer.source,\"update_chunk_request\",body)end if not sent then transfer.awaiting=false;return false,err end return true end local function beginDownload(source)local offer,err=findOffer(source)if not offer then return false,err end local compare=release.compareVersions(offer.version,version.string)if not compare or compare<=0 then return false,\"refusing a downgrade or current release\"end local free=fs.getFreeSpace and fs.getFreeSpace(\"/\")or nil if type(free)==\"number\"and free<offer.size+4096 then return false,\"not enough free space for one verified release cache\"end removePart()release.clear();cached=nil local value=loadState()value.transfer={id=\"update-\"..localId()..\"-\"..tostring(now())..\"-\"..tostring(math.random(1000,9999)),source=tostring(offer.id),label=offer.label,transport=offer.transport==\"routed\"and\"routed\"or\"direct\",profile=offer.profile,version=offer.version,size=offer.size,digest=offer.digest,keyId=offer.keyId,offset=1,received=0,retries=0,awaiting=false,started=now(),}setStatus(\"downloading\",\"Downloading signed release \"..offer.version..\" from \"..offer.label)record(\"download_started\",{id=offer.id,version=offer.version,transport=value.transfer.transport})local sent,sendErr=transferRequest(value.transfer)if not sent then value.transfer=nil;setStatus(\"error\",sendErr);record(\"download_failed\",{id=offer.id,error=sendErr})end save()return sent,sendErr end local function finishTransfer(transfer)local handle=fs.open(release.PART_FILE,\"r\")local source=handle and handle.readAll()or nil if handle then handle.close()end if not source or#source~=transfer.size then return false,\"download size did not match offer\"end local parsed,parseErr=release.parse(source)if not parsed then return false,parseErr end if parsed.manifest.profile~=profile.id()or parsed.manifest.profile~=transfer.profile or parsed.manifest.version~=transfer.version or parsed.manifest.digest:lower()~=transfer.digest:lower()or parsed.manifest.key_id~=transfer.keyId then return false,\"verified package did not match selected release offer\"end local metadata,promoteErr=release.promotePart(release.PART_FILE,transfer.label)if not metadata then return false,promoteErr end cached=nil;refreshCached()local value=loadState();value.transfer=nil setStatus(\"downloaded\",\"Verified release \"..metadata.version..\" is ready for local confirmation\")record(\"download_verified\",{id=transfer.source,version=metadata.version,digest=metadata.digest:sub(1,16)})save()return true,metadata end local function acceptChunk(source,body,transport)local value,transfer=loadState(),loadState().transfer if not transfer or transfer.transport~=transport or tostring(source)~=tostring(transfer.source)then return false,\"unexpected update chunk\"end if type(body)~=\"table\"or body.transfer~=transfer.id or math.floor(tonumber(body.offset)or 0)~=transfer.offset or type(body.data)~=\"string\"then return false,\"malformed update chunk\"end local remaining=transfer.size-transfer.offset+1 if#body.data<1 or#body.data>math.min(release.CHUNK_BYTES,remaining)then return false,\"invalid update chunk length\"end local ok,err=appendPart(body.data)if not ok then transfer.awaiting=false;setStatus(\"error\",err);record(\"download_failed\",{error=err});save();return false,err end transfer.offset,transfer.received,transfer.awaiting,transfer.retries=transfer.offset+#body.data,(transfer.received or 0)+#body.data,false,0 if transfer.offset>transfer.size then local complete,result=finishTransfer(transfer)if not complete then removePart();value.transfer=nil;setStatus(\"error\",result);record(\"download_failed\",{id=source,error=result});save()end return complete,result end local sent,sendErr=transferRequest(transfer)if not sent then setStatus(\"error\",sendErr);record(\"download_failed\",{id=source,error=sendErr});value.transfer=nil;removePart()end save()return sent,sendErr end local function serveChunk(target,body,routed,request)if type(body)~=\"table\"or not validId(body.transfer)or math.floor(tonumber(body.offset)or 0)<1 or math.floor(tonumber(body.length)or 0)<1 or tonumber(body.length)>release.CHUNK_BYTES then return false,\"invalid update chunk request\"end local source=cachedSource()if not source then return false,\"no verified newer signed release is cached\"end local offset,length=math.floor(body.offset),math.floor(body.length)if offset>#source then return false,\"requested update offset is beyond package\"end local data=source:sub(offset,math.min(#source,offset+length-1))local response={transfer=body.transfer,offset=offset,data=data}if routed then return net.reply(request,\"update_chunk\",response,{safe=true})end return net.publicSend(target,\"update_chunk\",response)end function M.publicPacket(packet,distance)local source,body=tostring(packet.from),packet.body or{}if packet.type==\"update_query\"then if body.profile~=profile.id()then return false,\"release profile does not match this installation\"end local offer=providerOffer()local compare=offer and release.compareVersions(offer.version,body.version or\"\")if offer and compare and compare>0 then net.publicSend(source,\"update_offer\",offer)end return elseif packet.type==\"update_offer\"then return upsertOffer(source,body,\"direct\",distance)elseif packet.type==\"update_chunk_request\"then return serveChunk(source,body,false)elseif packet.type==\"update_chunk\"then return acceptChunk(source,body,\"direct\")end end function M.authenticatedPacket(from,peer,packet)local body=packet.body or{}if packet.type==\"update_query\"then if body.profile~=profile.id()then return false,\"release profile does not match this installation\"end local offer=providerOffer()local compare=offer and release.compareVersions(offer.version,body.version or\"\")if offer and compare and compare>0 then return net.reply(packet,\"update_offer\",offer,{safe=true})end elseif packet.type==\"update_offer\"then return upsertOffer(from,body,\"routed\")elseif packet.type==\"update_chunk_request\"then return serveChunk(from,body,true,packet)elseif packet.type==\"update_chunk\"then return acceptChunk(from,body,\"routed\")end end function M.check()local value=loadState()for key in pairs(value.offers)do value.offers[key]=nil end setStatus(\"checking\",\"Looking for newer signed releases\")local sent,err=net.publicBroadcast(\"update_query\",{version=version.string,profile=profile.id()})local routed=0 for _,peer in ipairs(net.peers())do if not peer.rekeyRequired then local ok=net.request(peer.id,\"update_query\",{version=version.string,profile=profile.id()},{safe=true})if ok then routed=routed+1 end end end record(\"check\",{direct=sent==true,routed=routed,error=sent and nil or err})save()return sent or routed>0,sent and nil or err end function M.download(source)return beginDownload(source)end function M.cancel()local value=loadState()if not value.transfer then return false,\"no update transfer is active\"end record(\"download_cancelled\",{id=value.transfer.source,version=value.transfer.version})value.transfer=nil;removePart();setStatus(\"cancelled\",\"Update download cancelled\");save()return true end function M.clearCache()local value=loadState()if value.transfer then return false,\"cancel the active download first\"end release.clear();cached=nil;value.cache=nil;setStatus(\"idle\",\"Verified release cache cleared\");record(\"cache_cleared\");save()return true end function M.prepareApply()local package,metadata=release.cachedPackage()if not package then return false,metadata or\"no verified signed release is cached\"end local compare=release.compareVersions(package.manifest.version,version.string)if not compare or compare<=0 then return false,\"cached release is not newer than this CeetOS installation\"end if package.manifest.profile~=profile.id()then return false,\"cached release profile does not match this installation\"end local marker={schema=2,state=\"apply\",profile=package.manifest.profile,version=package.manifest.version,digest=package.manifest.digest,keyId=package.manifest.key_id,approvedAt=now()}store.write(release.PENDING_FILE,marker)local value=loadState();setStatus(\"applying\",\"Applying verified release \"..package.manifest.version..\" after reboot\");record(\"apply_approved\",{version=package.manifest.version,digest=package.manifest.digest:sub(1,16),keyId=package.manifest.key_id});save()return true,{version=package.manifest.version,fingerprint=package.manifest.digest:sub(1,16),keyId=package.manifest.key_id}end function M.recover()local marker=store.read(release.PENDING_FILE,nil)if type(marker)~=\"table\"or marker.state~=\"verify\"then return end local value=loadState()if marker.version==version.string and(marker.profile==nil or marker.profile==profile.id())then if fs.exists(release.PENDING_FILE)then fs.delete(release.PENDING_FILE)end setStatus(\"applied\",\"Verified release \"..version.string..\" is now active\")record(\"apply_verified\",{version=version.string})else setStatus(\"error\",\"Update installed but running version did not match \"..tostring(marker.version))record(\"apply_version_mismatch\",{expected=marker.version,actual=version.string})end save()end function M.tick()local value,timestamp=loadState(),now()local offer=providerOffer()if offer and timestamp-lastBeacon>=BEACON_INTERVAL then net.publicBroadcast(\"update_offer\",offer)lastBeacon=timestamp end local transfer=value.transfer if transfer and transfer.awaiting and timestamp-(transfer.requestedAt or timestamp)>=REQUEST_TIMEOUT then transfer.awaiting,transfer.retries=false,(transfer.retries or 0)+1 if transfer.retries>REQUEST_LIMIT then record(\"download_timeout\",{id=transfer.source,version=transfer.version})value.transfer=nil;removePart();setStatus(\"error\",\"Update source timed out\");save()else local sent,err=transferRequest(transfer)if not sent then value.transfer=nil;removePart();setStatus(\"error\",err);record(\"download_failed\",{id=transfer.source,error=err});save()end end end end function M.status()local value=loadState()return{profile=profile.id(),version=version.string,status=value.status,message=value.message,revision=value.revision,offers=offers(),transfer=value.transfer,cache=value.cache,history=value.history,cacheRoot=release.CACHE_ROOT,maxPackageBytes=release.MAX_PACKAGE_BYTES,}end return M",
  ["ceetos/lib/mesh_crypto.lua"] = "local password=require(\"ceetos.lib.password\")local M={}local band,bor,bxor=bit32.band,bit32.bor,bit32.bxor local lshift,rshift=bit32.lshift,bit32.rshift local MOD32,LIMB=4294967296,67108864 local function add32(...)local n=0 for i=1,select(\"#\",...)do n=(n+(select(i,...)or 0))%MOD32 end return n end local function rotl(v,bits)return bor(lshift(v,bits),rshift(v,32-bits))end local function le32(data,index)local a,b,c,d=data:byte(index,index+3)return(a or 0)+(b or 0)*256+(c or 0)*65536+(d or 0)*16777216 end local function put32(value)value=value%MOD32 return string.char(value%256,math.floor(value/256)%256,math.floor(value/65536)%256,math.floor(value/16777216)%256)end local function le64(value)value=math.max(0,math.floor(tonumber(value)or 0))local low,high=value%MOD32,math.floor(value/MOD32)%MOD32 return put32(low)..put32(high)end local function hexToRaw(value)if type(value)~=\"string\"or#value%2~=0 or value:find(\"[^%x]\")then return nil end return(value:gsub(\"..\",function(pair)return string.char(tonumber(pair,16))end))end local function rawToHex(value)return(value:gsub(\".\",function(byte)return string.format(\"%02x\",byte:byte())end))end local function xorBytes(left,right)local out={}for i=1,#left do out[i]=string.char(bxor(left:byte(i),right:byte(i)))end return table.concat(out)end local function quarter(x,a,b,c,d)x[a]=add32(x[a],x[b]);x[d]=rotl(bxor(x[d],x[a]),16)x[c]=add32(x[c],x[d]);x[b]=rotl(bxor(x[b],x[c]),12)x[a]=add32(x[a],x[b]);x[d]=rotl(bxor(x[d],x[a]),8)x[c]=add32(x[c],x[d]);x[b]=rotl(bxor(x[b],x[c]),7)end local function chachaBlock(key,counter,nonce)local state={0x61707865,0x3320646e,0x79622d32,0x6b206574}for i=1,8 do state[4+i]=le32(key,(i-1)*4+1)end state[13],state[14],state[15],state[16]=counter%MOD32,le32(nonce,1),le32(nonce,5),le32(nonce,9)local x={}for i=1,16 do x[i]=state[i]end for _=1,10 do quarter(x,1,5,9,13);quarter(x,2,6,10,14);quarter(x,3,7,11,15);quarter(x,4,8,12,16)quarter(x,1,6,11,16);quarter(x,2,7,12,13);quarter(x,3,8,9,14);quarter(x,4,5,10,15)end local out={}for i=1,16 do out[i]=put32(add32(x[i],state[i]))end return table.concat(out)end local function chachaXor(key,nonce,counter,plaintext)local out,position={},1 while position<=#plaintext do local block=chachaBlock(key,counter,nonce)local part=plaintext:sub(position,position+63)out[#out+1]=xorBytes(part,block:sub(1,#part))position,counter=position+#part,(counter+1)%MOD32 end return table.concat(out)end local function poly1305(message,key)local function zero(length)local out={};for i=1,length do out[i]=0 end;return out end local function normalise(value)local carry=0 for i=1,#value do local n=(value[i]or 0)+carry value[i],carry=n%256,math.floor(n/256)end while carry>0 do value[#value+1],carry=carry%256,math.floor(carry/256)end return value end local function add(left,right)local out=zero(math.max(#left,#right)+1)for i=1,#left do out[i]=out[i]+left[i]end for i=1,#right do out[i]=out[i]+right[i]end return normalise(out)end local function multiply(left,right)local out=zero(#left+#right+1)for i=1,#left do for j=1,#right do out[i+j-1]=out[i+j-1]+left[i]*right[j]end end return normalise(out)end local function high130(value)local out={}for i=1,#value-16 do local low=value[i+16]or 0 local high=value[i+17]or 0 out[i]=math.floor(low/4)+(high%4)*64 end return normalise(out)end local function hasHigh(value)if#value>17 then for i=18,#value do if value[i]~=0 then return true end end end return(value[17]or 0)>=4 end local function reduce(value)while hasHigh(value)do local low,high=zero(17),high130(value)for i=1,17 do low[i]=value[i]or 0 end low[17]=low[17]%4 for i=1,#high do high[i]=high[i]*5 end value=add(low,high)end local prime={251}for i=2,16 do prime[i]=255 end prime[17]=3 local greater=false for i=17,1,-1 do if(value[i]or 0)~=prime[i]then greater=(value[i]or 0)>prime[i];break end end if greater then local borrow=0 for i=1,17 do local n=(value[i]or 0)-prime[i]-borrow if n<0 then n,borrow=n+256,1 else borrow=0 end value[i]=n end end return value end local r,h=zero(17),zero(17)for i=1,16 do r[i]=key:byte(i)end r[4],r[8],r[12],r[16]=band(r[4],15),band(r[8],15),band(r[12],15),band(r[16],15)r[5],r[9],r[13]=band(r[5],252),band(r[9],252),band(r[13],252)for at=1,#message,16 do local part,n=message:sub(at,at+15),zero(17)for i=1,#part do n[i]=part:byte(i)end n[#part+1]=1 h=reduce(multiply(add(h,n),r))end local pad,out,carry={},{},0 for i=1,16 do pad[i]=key:byte(i+16)end for i=1,16 do local n=(h[i]or 0)+pad[i]+carry out[i],carry=n%256,math.floor(n/256)end return string.char(table.unpack(out))end local function padded(value)return value..string.rep(\"\\0\",(16-(#value%16))%16)end local function derive(linkKey)local raw=hexToRaw(password.hmacSha256(linkKey,\"ceetos/mesh/aead-key/v1\")or\"\")return raw end function M.nonce(linkKey,counter,context)local raw=hexToRaw(password.hmacSha256(linkKey,\"ceetos/mesh/aead-nonce/v1|\"..tostring(counter)..\"|\"..tostring(context or\"\"))or\"\")return raw and raw:sub(1,12)or nil end function M.seal(linkKey,nonce,aad,plaintext)if type(linkKey)~=\"string\"or type(nonce)~=\"string\"or#nonce~=12 or type(aad)~=\"string\"or type(plaintext)~=\"string\"then return nil,\"invalid mesh envelope input\"end local key=derive(linkKey);if not key or#key~=32 then return nil,\"could not derive mesh envelope key\"end local polyKey=chachaBlock(key,0,nonce):sub(1,32)local ciphertext=chachaXor(key,nonce,1,plaintext)local tag=poly1305(padded(aad)..padded(ciphertext)..le64(#aad)..le64(#ciphertext),polyKey)return{nonce=rawToHex(nonce),ciphertext=rawToHex(ciphertext),tag=rawToHex(tag)}end function M.open(linkKey,envelope,aad)if type(linkKey)~=\"string\"or type(envelope)~=\"table\"or type(aad)~=\"string\"then return nil,\"invalid mesh envelope\"end local nonce,ciphertext,tag=hexToRaw(envelope.nonce or\"\"),hexToRaw(envelope.ciphertext or\"\"),hexToRaw(envelope.tag or\"\")if not nonce or#nonce~=12 or not ciphertext or not tag or#tag~=16 then return nil,\"invalid mesh envelope encoding\"end local key=derive(linkKey);if not key or#key~=32 then return nil,\"could not derive mesh envelope key\"end local polyKey=chachaBlock(key,0,nonce):sub(1,32)local expected=rawToHex(poly1305(padded(aad)..padded(ciphertext)..le64(#aad)..le64(#ciphertext),polyKey))if not password.constantTimeEqual(expected,envelope.tag)then return nil,\"mesh envelope authentication failed\"end return chachaXor(key,nonce,1,ciphertext)end function M.selfTest()local key=hexToRaw(\"85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b\")local tag=poly1305(\"Cryptographic Forum Research Group\",key)return rawToHex(tag)==\"a8061dc1305136c6c22b8baf0c0127a9\"end return M",
  ["ceetos/lib/audit.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local PATH=\"/ceetos/data/audit.log\"M.LIMITS={maxEntries=250,maxBytes=64*1024}function M.log(actor,action,detail)return store.append(PATH,{ts=os.epoch(\"utc\"),actor=actor or\"system\",action=action,detail=detail,},M.LIMITS)end function M.recent(limit)limit=math.max(1,math.min(50,tonumber(limit)or 10))local raw=store.readAppend(PATH)local result={}for line in(raw..\"\\n\"):gmatch(\"(.-)\\n\")do if line~=\"\"then local entry=textutils.unserialise(line)if entry then result[#result+1]=entry if#result>limit then table.remove(result,1)end end end end return result end return M",
  ["ceetos/lib/net.lua"] = "local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local password=require(\"ceetos.lib.password\")local meshCrypto=require(\"ceetos.lib.mesh_crypto\")local M={handler=nil,publicHandler=nil}local PATH=\"/ceetos/data/network.lua\"local SECURITY_PATH=\"/ceetos/data/network-security.lua\"local SEEN_PATH=\"/ceetos/data/seen-peers.lua\"local CHANNEL,PROTOCOL=45731,\"ceetos/2\"local PUBLIC_UPDATE_PROTOCOL=\"ceetos/update/1\"local PUBLIC_UPDATE_TYPES={update_query=true,update_offer=true,update_chunk_request=true,update_chunk=true,}local DISCOVERY_CHANNEL=CHANNEL local DISCOVERY_TTL,ROUTE_TTL,PACKET_TTL=120000,120000,8 local DISCOVERY_INTERVAL,PAIR_RETRY,ROUTE_INTERVAL=2000,2000,15000 local MESH_BEACON_INTERVAL,MESH_FULL_INTERVAL=15000,30000 local PROBE_INTERVAL,PROBE_DEGRADED,PROBE_OFFLINE,PROBE_RECOVERY=5000,3,6,10000 local ROUTE_CANDIDATE_LIMIT,ROUTE_BUCKET_LIMIT=4,96 local SAFE_READ_WINDOW,SAFE_READ_LIMIT=1000,16 local MAX_PACKET_STRING,MAX_PACKET_TABLE,MAX_PACKET_DEPTH=8192,128,6 local COUNTER_RESERVATION,MAX_RX_EPOCHS,MAX_REVERSE_ROUTES=64,3,128 local routes,discovered,confirmations,seenPackets,reverseRoutes={},{},{},{},{}local routeSequence,lastRouteAdvertisement,routesDirty=0,0,false local discovery,lastDiscovery=nil,{state=\"inactive\"}local security={tx={},rx={}}local autoEdges,linkHealth,meshOffers,meshSeen,advertisedRoutes,meshEnrollmentSends={},{},{},{},{},{}local lastMeshBeacon,lastMeshFull,meshDirty=0,0,false local transport={modem=false,wireless=false,name=nil,channels={},lastTransmit=nil,lastReceive=nil,lastDistance=nil,lastError=nil,rejected=0,}local traces={discovery={},routing={},transport={}}local TRACE_LIMIT=48 local MESH_STATE_LIMIT=128 local rejectPacket local function now()return os.epoch(\"utc\")end local function id()return tostring(os.getComputerID())end local function label()return os.getComputerLabel()or(\"Computer \"..id())end local function isBroadcast(value)return tostring(value)==\"0\"end local function isForUs(value)return tostring(value)==id()end local function copy(value,depth)if type(value)~=\"table\"then return value end if(depth or 0)>MAX_PACKET_DEPTH then return nil end local out={}for k,v in pairs(value)do out[k]=copy(v,(depth or 0)+1)end return out end local function trace(kind,phase,fields)local bucket=traces[kind]or traces.transport local row={time=now(),phase=phase}for key,value in pairs(fields or{})do if key~=\"phrase\"and key~=\"proof\"and key~=\"key\"and key~=\"secret\"and key~=\"mac\"and key~=\"packet\"and key~=\"tag\"then row[key]=value end end bucket[#bucket+1]=row while#bucket>TRACE_LIMIT do table.remove(bucket,1)end end local function read(path,fallback)if store.invalidate then store.invalidate(path)end return store.read(path,fallback)end local function config()local value=read(PATH,{peers={},channel=CHANNEL})value.peers=type(value.peers)==\"table\"and value.peers or{}value.channel=tonumber(value.channel)or CHANNEL return value end local function save(value)local ok,err=store.write(PATH,value)if ok==false then error(err or\"could not save network configuration\",0)end end local function migrateV1Links()local value,changed=config(),false for _,peer in pairs(value.peers)do if peer.protocol~=2 or peer.rekeyRequired or type(peer.key)~=\"string\"or#peer.key<32 then peer.protocol,peer.rekeyRequired,peer.key=1,true,nil peer.secret=nil changed=true end end if changed then save(value)end return value end local function activePeer(value,peerId)local peer=value.peers[tostring(peerId)]if peer and peer.protocol==2 and peer.rekeyRequired~=true and type(peer.key)==\"string\"and#peer.key>=32 then return peer end return nil end local function activeLink(value,peerId)local manual=activePeer(value,peerId)if manual then return manual,\"direct\"end local edge=autoEdges[tostring(peerId)]if edge and type(edge.key)==\"string\"and#edge.key>=32 and(edge.expires or 0)>now()then return edge,\"mesh\"end return nil end local function healthFor(peerId)peerId=tostring(peerId)local state=linkHealth[peerId]if not state then state={state=\"healthy\",successes=0,misses=0,lastAuthenticated=now(),stableSince=now(),inFlight=0}linkHealth[peerId]=state end return state end local function linkState(peerId)return healthFor(peerId).state or\"healthy\"end local function currentInFlight(state,timestamp)state.inFlightUntil=type(state.inFlightUntil)==\"table\"and state.inFlightUntil or{}local kept={}for _,expires in ipairs(state.inFlightUntil)do if expires>timestamp then kept[#kept+1]=expires end end state.inFlightUntil,state.inFlight=kept,#kept return state.inFlight end local function markInFlight(state)local timestamp=now()currentInFlight(state,timestamp)state.inFlightUntil[#state.inFlightUntil+1]=timestamp+SAFE_READ_WINDOW table.sort(state.inFlightUntil)while#state.inFlightUntil>SAFE_READ_LIMIT do table.remove(state.inFlightUntil,1)end state.inFlight=#state.inFlightUntil end local function linkUsable(value,peerId,allowDegraded)if not activeLink(value,peerId)then return false end local state=linkState(peerId)return state==\"healthy\"or(allowDegraded and state==\"degraded\")end local function touchLink(peerId,distance,source)local state,timestamp=healthFor(peerId),now()state.lastAuthenticated,state.lastDistance,state.lastSource=timestamp,distance,source or\"traffic\"state.misses,state.awaiting,state.probeToken=0,false,nil state.loss=math.max(0,(state.loss or 0)*0.5)if state.state~=\"healthy\"then state.successes=(state.successes or 0)+1 if state.successes>=2 then state.state,state.stableSince,state.recoveredAt=\"healthy\",timestamp,timestamp routesDirty=true trace(\"routing\",\"link_recovered\",{peer=peerId,source=source})end else state.successes=math.min(2,(state.successes or 0)+1)end end local function allDirectLinks(value)local out,seen={},{}for peerId in pairs(value.peers)do if activePeer(value,peerId)then out[#out+1],seen[peerId]=tostring(peerId),true end end for peerId,edge in pairs(autoEdges)do if not seen[peerId]and edge.expires>now()then out[#out+1]=tostring(peerId)end end table.sort(out,function(a,b)return tonumber(a)<tonumber(b)end)return out end local function remember(peer,peerLabel,extra)local value=read(SEEN_PATH,{})local old=value[tostring(peer)]or{}old.label,old.seen=peerLabel or old.label or(\"Computer \"..tostring(peer)),now()if extra then for key,item in pairs(extra)do if key~=\"tag\"and key~=\"proof\"then old[key]=item end end end value[tostring(peer)]=old store.write(SEEN_PATH,value)end local function modem(requireWireless)local candidates,used={},{}local sides=rs and rs.getSides and rs.getSides()or{}for _,name in ipairs(sides)do if peripheral.getType(name)==\"modem\"and not used[name]then candidates[#candidates+1],used[name]=name,true end end if peripheral.getNames then for _,name in ipairs(peripheral.getNames())do if peripheral.getType(name)==\"modem\"and not used[name]then candidates[#candidates+1],used[name]=name,true end end end local fallback for _,name in ipairs(candidates)do local wrapped=peripheral.wrap(name)if wrapped then fallback=fallback or wrapped local ok,wireless=pcall(function()return not wrapped.isWireless or wrapped.isWireless()end)if ok and wireless then return wrapped end end end return requireWireless and nil or fallback end local function modems()local out,seen={},{}local function add(name)if not seen[name]and peripheral.getType(name)==\"modem\"then local wrapped=peripheral.wrap(name)if wrapped then out[#out+1],seen[name]={name=name,modem=wrapped},true end end end for _,name in ipairs(rs and rs.getSides and rs.getSides()or{})do add(name)end for _,name in ipairs(peripheral.getNames and peripheral.getNames()or{})do add(name)end return out end local function wirelessModems()local out={}for _,item in ipairs(modems())do local ok,wireless=pcall(function()return not item.modem.isWireless or item.modem.isWireless()end)if ok and wireless then out[#out+1]=item end end return out end local function openChannels(value)local channels={value.channel or CHANNEL}if channels[1]~=DISCOVERY_CHANNEL then channels[#channels+1]=DISCOVERY_CHANNEL end transport.channels=channels local all=modems()if#all==0 then transport.lastError=\"no modem attached\";trace(\"transport\",\"modem_missing\");return channels end for _,item in ipairs(all)do for _,channel in ipairs(channels)do local ok,err=pcall(item.modem.open,channel)if not ok then transport.lastError=tostring(err)trace(\"transport\",\"channel_open_failed\",{channel=channel,error=transport.lastError,modem=item.name})end end end return channels end local function canonical(value,depth)depth=depth or 0 local kind=type(value)if kind==\"nil\"then return\"n\"end if kind==\"boolean\"then return value and\"b1\"or\"b0\"end if kind==\"number\"then if value~=value or value==math.huge or value==-math.huge then return nil,\"invalid numeric packet value\"end return\"d\"..string.format(\"%.17g\",value)..\";\"end if kind==\"string\"then if#value>MAX_PACKET_STRING then return nil,\"packet string exceeds limit\"end return\"s\"..tostring(#value)..\":\"..value end if kind~=\"table\"then return nil,\"unsupported packet value\"end if depth>=MAX_PACKET_DEPTH then return nil,\"packet nesting exceeds limit\"end local entries={}for key,item in pairs(value)do if#entries>=MAX_PACKET_TABLE then return nil,\"packet table exceeds limit\"end local encodedKey,keyErr=canonical(key,depth+1);if not encodedKey then return nil,keyErr end local encodedValue,valueErr=canonical(item,depth+1);if not encodedValue then return nil,valueErr end entries[#entries+1]=encodedKey..\"=\"..encodedValue end table.sort(entries)return\"t\"..tostring(#entries)..\"{\"..table.concat(entries,\",\")..\"}\"end local function packetMaterial(packet)return canonical({protocol=packet.protocol,type=packet.type,from=packet.from,to=packet.to,origin=packet.origin,destination=packet.destination,ttl=packet.ttl,request_id=packet.request_id,timestamp=packet.timestamp,reply_to=packet.reply_to,auth=packet.auth and{epoch=packet.auth.epoch,counter=packet.auth.counter}or nil,confidential=packet.confidential==true,body=packet.body,})end local function confidentialAad(packet)return table.concat({\"ceetos/confidential/v1\",tostring(packet.type),tostring(packet.origin),tostring(packet.destination),tostring(packet.request_id)},\"|\")end local function sealBody(peer,packet)local encoded=textutils.serialise(packet.body or{},{compact=true})if#encoded>6144 then return nil,\"confidential payload exceeds limit\"end local nonce=meshCrypto.nonce(peer.key,password.newSalt(\"confidential:\"..tostring(packet.request_id)),confidentialAad(packet))if not nonce then return nil,\"could not create confidential nonce\"end local envelope,err=meshCrypto.seal(peer.key,nonce,confidentialAad(packet),encoded)if not envelope then return nil,err end return{envelope=envelope}end local function openBody(peer,packet)if type(packet.body)~=\"table\"or type(packet.body.envelope)~=\"table\"then return nil,\"malformed confidential envelope\"end local decoded,err=meshCrypto.open(peer.key,packet.body.envelope,confidentialAad(packet))if not decoded or#decoded>6144 then return nil,err or\"invalid confidential payload\"end local ok,value=pcall(textutils.unserialise,decoded)if not ok or type(value)~=\"table\"then return nil,\"invalid confidential payload\"end return value end local function loadSecurity()security=read(SECURITY_PATH,{tx={},rx={}})security.tx=type(security.tx)==\"table\"and security.tx or{}security.rx=type(security.rx)==\"table\"and security.rx or{}end local function saveSecurity()local ok,err=store.write(SECURITY_PATH,security)if ok==false then error(err or\"could not persist peer replay state\",0)end end local function counter(peerId)local key=tostring(peerId)local tx=security.tx[key]if not tx then tx={epoch=password.newSalt(\"peer-tx:\"..key),reserved=0};security.tx[key]=tx end tx.next=tonumber(tx.next)or((tonumber(tx.reserved)or 0)+1)if tx.next>(tonumber(tx.reserved)or 0)then tx.reserved=tx.next+COUNTER_RESERVATION-1 saveSecurity()end local value=tx.next;tx.next=value+1 return tx.epoch,value end local function verifyReplay(from,auth)if type(auth)~=\"table\"or type(auth.epoch)~=\"string\"or#auth.epoch<16 or#auth.epoch>128 then return false,\"invalid packet epoch\"end local sequence=tonumber(auth.counter)if not sequence or sequence<1 or sequence%1~=0 then return false,\"invalid packet counter\"end local key=tostring(from)local row=security.rx[key]or{epochs={}}row.epochs=type(row.epochs)==\"table\"and row.epochs or{}local old=tonumber(row.epochs[auth.epoch])if old and sequence<=old then return false,\"replayed packet counter\"end row.epochs[auth.epoch]=sequence local order={}for epoch,max in pairs(row.epochs)do order[#order+1]={epoch=epoch,max=tonumber(max)or 0}end table.sort(order,function(a,b)return a.max>b.max end)while#order>MAX_RX_EPOCHS do row.epochs[table.remove(order).epoch]=nil end security.rx[key]=row saveSecurity()return true end local function newRequestId()return id()..\":\"..password.newSalt(\"peer-request\")end local function sign(peerId,peer,packet)local epoch,sequence=counter(peerId)packet.auth={epoch=epoch,counter=sequence}local material,err=packetMaterial(packet)if not material then return false,err end local mac,macErr=password.hmacSha256(peer.key,material)if not mac then return false,macErr end packet.auth.mac=mac return true end local function verifyPacket(packet,value)if type(packet)~=\"table\"then return false,\"packet is not a table\"end if packet.protocol~=PROTOCOL then return false,packet.protocol==\"ceetos/1\"and\"v1 packet rejected; rekey required\"or\"invalid packet protocol\"end if type(packet.type)~=\"string\"or#packet.type==0 or#packet.type>64 then return false,\"invalid packet type\"end if type(packet.from)~=\"string\"and type(packet.from)~=\"number\"then return false,\"invalid packet sender\"end local from=tostring(packet.from)local peer=activeLink(value,from)if not peer then return false,\"sender is not an active direct or mesh peer\"end if not isForUs(packet.to)then return false,\"packet addressed to another hop\"end if type(packet.origin)~=\"string\"and type(packet.origin)~=\"number\"then return false,\"invalid packet origin\"end if type(packet.destination)~=\"string\"and type(packet.destination)~=\"number\"then return false,\"invalid packet destination\"end if type(packet.request_id)~=\"string\"or#packet.request_id==0 or#packet.request_id>128 then return false,\"invalid packet request ID\"end if packet.reply_to~=nil and(type(packet.reply_to)~=\"string\"or#packet.reply_to==0 or#packet.reply_to>128)then return false,\"invalid reply correlation ID\"end local ttl=tonumber(packet.ttl)if not ttl or ttl%1~=0 or ttl<1 or ttl>PACKET_TTL then return false,\"invalid packet TTL\"end local timestamp=tonumber(packet.timestamp)if not timestamp or math.abs(now()-timestamp)>ROUTE_TTL then return false,\"stale packet timestamp\"end if type(packet.auth)~=\"table\"or type(packet.auth.mac)~=\"string\"or#packet.auth.mac~=64 then return false,\"missing packet MAC\"end local receivedMac=packet.auth.mac packet.auth.mac=nil local material,materialErr=packetMaterial(packet)packet.auth.mac=receivedMac if not material then return false,materialErr end local expected,macErr=password.hmacSha256(peer.key,material)if not expected or not password.constantTimeEqual(expected,receivedMac)then return false,macErr or\"invalid packet MAC\"end local replayOk,replayErr=verifyReplay(from,packet.auth)if not replayOk then return false,replayErr end return true,peer end local function routeBucket(targetId)local bucket=routes[tostring(targetId)]if bucket and bucket.next then bucket={candidates={{next=tostring(bucket.next),hops=tonumber(bucket.hops)or PACKET_TTL,expires=tonumber(bucket.expires)or 0,label=bucket.label,mode=bucket.mode,masterId=bucket.masterId,legacy=true,loss=1,}},primary=tostring(bucket.next),changedAt=now(),reason=\"legacy route\"}routes[tostring(targetId)]=bucket end if not bucket then bucket={candidates={},changedAt=now(),reason=\"new\"}routes[tostring(targetId)]=bucket end bucket.candidates=type(bucket.candidates)==\"table\"and bucket.candidates or{}return bucket end local function candidateHealth(candidate)local state=healthFor(candidate.next)local rank=state.state==\"healthy\"and 0 or(state.state==\"degraded\"and 1 or 2)return rank,state end local function directCandidate(value,targetId)local link,kind=activeLink(value,targetId)if link then return{next=targetId,hops=1,expires=now()+ROUTE_TTL,kind=kind,direct=true,label=link.label,mode=link.mode,masterId=link.masterId}end end local function validCandidates(targetId,value)local timestamp,out,seen=now(),{},{}local direct=directCandidate(value,targetId)if direct then out[#out+1],seen[direct.next..\":\"..direct.kind]=direct,true end local bucket=routes[tostring(targetId)]if bucket then for _,candidate in ipairs(routeBucket(targetId).candidates)do local key=tostring(candidate.next)..\":\"..tostring(candidate.kind or\"route\")if not seen[key]and candidate.expires>timestamp and candidate.hops>=1 and candidate.hops<=PACKET_TTL and activeLink(value,candidate.next)then out[#out+1],seen[key]=candidate,true end end end return out end local function compareCandidates(a,b)local ar,ah=candidateHealth(a)local br,bh=candidateHealth(b)if ar~=br then return ar<br end if(a.hops or PACKET_TTL)~=(b.hops or PACKET_TTL)then return(a.hops or PACKET_TTL)<(b.hops or PACKET_TTL)end if(ah.loss or 0)~=(bh.loss or 0)then return(ah.loss or 0)<(bh.loss or 0)end if(ah.rtt or 0)~=(bh.rtt or 0)then return(ah.rtt or 0)<(bh.rtt or 0)end return tostring(a.next)<tostring(b.next)end local function primaryCandidate(targetId,value,safe)local candidates=validCandidates(targetId,value)if#candidates==0 then return nil end table.sort(candidates,compareCandidates)local target,bucket,selected=tostring(targetId),routeBucket(targetId),candidates[1]local existing for _,item in ipairs(candidates)do if tostring(item.next)==tostring(bucket.primary)then existing=item;break end end if existing then local oldRank,newRank=candidateHealth(existing),candidateHealth(selected)if oldRank<newRank then selected=existing elseif oldRank==newRank and oldRank==0 and(selected.hops or 99)<(existing.hops or 99)then local health=healthFor(selected.next)if(health.successes or 0)<2 or(health.recoveredAt and now()-(health.stableSince or now())<PROBE_RECOVERY)then selected=existing end end end if safe then local rank,_,choices=candidateHealth(selected),nil,{}for _,item in ipairs(candidates)do local itemRank=candidateHealth(item)if itemRank==rank and item.hops==selected.hops then choices[#choices+1]=item end end if#choices>1 then table.sort(choices,function(a,b)local ah,bh=healthFor(a.next),healthFor(b.next)local aLoad,bLoad=currentInFlight(ah,now()),currentInFlight(bh,now())if aLoad~=bLoad then return aLoad<bLoad end return tostring(a.next)<tostring(b.next)end)selected=choices[1]end end if bucket.primary~=tostring(selected.next)then bucket.primary,bucket.changedAt,bucket.reason=tostring(selected.next),now(),existing and\"health/hop selection\"or\"initial selection\"trace(\"routing\",\"route_selected\",{destination=target,next=selected.next,hops=selected.hops,reason=bucket.reason})end return selected end local function nextHop(target,safe)local candidate=primaryCandidate(target,config(),safe)if candidate then local health=healthFor(candidate.next)if safe then markInFlight(health)end health.lastSelected=now()return candidate.next,candidate end routes[tostring(target)]=nil end local function transmit(items,channel,packet)if#items==0 then return false,\"no compatible modem attached\"end local sent,lastError=false,nil for _,item in ipairs(items)do local ok,err=pcall(item.modem.transmit,channel,channel,packet)if ok then sent=true else lastError=tostring(err)end end if not sent then transport.lastError=lastError or\"modem transmit failed\"end return sent,transport.lastError end local function send(target,input,forcedHop)local hop=forcedHop or nextHop(target,input.routeSafe==true and input.balanceSafe==true)if not hop then return false,\"peer is unreachable or requires rekey\"end local value,peer=config(),activeLink(config(),hop)if not peer then return false,\"direct route is unavailable\"end local packet=copy(input)packet.auth,packet.protocol,packet.from,packet.to=nil,PROTOCOL,id(),hop packet.origin,packet.destination=tostring(packet.origin or id()),tostring(packet.destination or target)packet.ttl=math.floor(tonumber(packet.ttl)or PACKET_TTL)packet.timestamp=now()if packet.ttl<1 or packet.ttl>PACKET_TTL then return false,\"invalid packet TTL\"end if type(packet.request_id)~=\"string\"or#packet.request_id==0 then packet.request_id=newRequestId()end if packet.confidential==true then local sealed,sealErr=sealBody(peer,packet)if not sealed then return false,sealErr end packet.body=sealed end local signed,signErr=sign(hop,peer,packet)if not signed then return false,signErr end openChannels(value)local links=wirelessModems()if#links==0 then return false,\"no wireless modem attached\"end local sent,err=transmit(links,value.channel or CHANNEL,packet)if not sent then trace(\"transport\",\"transmit_failed\",{type=packet.type,to=hop,error=err});return false,err end transport.lastTransmit,transport.lastError=now(),nil healthFor(hop).lastTransmit=now()trace(\"routing\",\"packet_sent\",{type=packet.type,to=hop,destination=packet.destination,links=#links})return true end local function rememberReverse(packet,previousHop)if type(packet)~=\"table\"or type(packet.request_id)~=\"string\"or#packet.request_id==0 then return false end local requestId,origin=packet.request_id,tostring(packet.origin)local existing=reverseRoutes[requestId]if existing and(existing.previous~=tostring(previousHop)or existing.origin~=origin)then return false,\"reverse route collision\"end reverseRoutes[requestId]={previous=tostring(previousHop),origin=origin,expires=now()+ROUTE_TTL}local entries={}for key,route in pairs(reverseRoutes)do entries[#entries+1]={id=key,expires=route.expires or 0}end table.sort(entries,function(a,b)return a.expires<b.expires end)while#entries>MAX_REVERSE_ROUTES do reverseRoutes[table.remove(entries,1).id]=nil end return true end local function broadcast(packet)local links=wirelessModems()if#links==0 then transport.lastError=\"no wireless modem attached\";trace(\"discovery\",\"wireless_missing\");return false,transport.lastError end local out=copy(packet)out.protocol,out.from,out.to=PROTOCOL,id(),0 out.timestamp=now()openChannels(config())local sent,err=transmit(links,DISCOVERY_CHANNEL,out)if not sent then trace(\"discovery\",\"transmit_failed\",{type=out.type,error=err});return false,err end transport.lastTransmit,transport.lastError=now(),nil trace(\"discovery\",out.type==\"discover\"and\"announcement_sent\"or\"discovery_packet_sent\",{type=out.type})return true end local function publicTransmit(target,typeName,body)if not PUBLIC_UPDATE_TYPES[typeName]then return false,\"invalid public update packet type\"end local encoded,encodeErr=canonical(body or{})if not encoded then return false,encodeErr or\"invalid public update body\"end local links=wirelessModems()if#links==0 then transport.lastError=\"no wireless modem attached\";return false,transport.lastError end openChannels(config())local packet={protocol=PUBLIC_UPDATE_PROTOCOL,type=typeName,from=id(),to=tostring(target),timestamp=now(),request_id=newRequestId(),body=body or{},}local sent,err=transmit(links,DISCOVERY_CHANNEL,packet)if not sent then transport.lastError=err or\"public update transmit failed\"trace(\"transport\",\"public_update_transmit_failed\",{type=typeName,to=target,error=transport.lastError})return false,transport.lastError end transport.lastTransmit,transport.lastError=now(),nil trace(\"transport\",\"public_update_sent\",{type=typeName,to=target,bytes=#encoded})return true end function M.publicSend(target,typeName,body)if tostring(target)==\"0\"or tostring(target)==\"\"then return false,\"public update target must be a computer ID\"end return publicTransmit(target,typeName,body)end function M.publicBroadcast(typeName,body)return publicTransmit(\"0\",typeName,body)end local function handshake(target,packet)local links=wirelessModems()if#links==0 then transport.lastError=\"no wireless modem attached\";return false,transport.lastError end local out=copy(packet)out.protocol,out.from,out.to,out.timestamp=PROTOCOL,id(),tostring(target),now()out.destination,out.ttl=tostring(target),1 openChannels(config())local sent,err=transmit(links,config().channel or CHANNEL,out)if not sent then trace(\"discovery\",\"handshake_transmit_failed\",{type=out.type,to=target,error=err});return false,err end transport.lastTransmit=now()trace(\"discovery\",out.type..\"_sent\",{to=target})return true end local function phraseTag(phrase,sessionId,hostId)return password.hmacSha256(phrase,\"ceetos/discovery/v2|\"..sessionId..\"|\"..tostring(hostId))end local function joinProof(phrase,sessionId,hostId,joinerId,nonce)return password.hmacSha256(phrase,\"ceetos/pair/join/v2|\"..sessionId..\"|\"..tostring(hostId)..\"|\"..tostring(joinerId)..\"|\"..nonce)end local function confirmProof(phrase,sessionId,hostId,joinerId,nonce,mode,master)return password.hmacSha256(phrase,\"ceetos/pair/confirm/v2|\"..sessionId..\"|\"..tostring(hostId)..\"|\"..tostring(joinerId)..\"|\"..nonce..\"|\"..tostring(mode)..\"|\"..tostring(master or\"\"))end local function linkKey(phrase,sessionId,hostId,joinerId,nonce)return password.derive(phrase,\"ceetos/link/v2|\"..sessionId..\"|\"..tostring(hostId)..\"|\"..tostring(joinerId)..\"|\"..nonce,64)end local function keyId(key)local value=password.hmacSha256(key,\"ceetos/key-id/v2\")return value and value:sub(1,16)or\"\"end local function activeSession()return discovery and(discovery.state==\"hosting\"or discovery.state==\"searching\"or discovery.state==\"candidate\"or discovery.state==\"confirming\")end local function setLast(state,message)lastDiscovery={state=state,message=message,at=now()}end local function persistPair(peerId,key,metadata,masterId)local value=config()value.peers[tostring(peerId)]={protocol=2,rekeyRequired=false,key=key,keyId=keyId(key),role=\"operator\",mode=metadata.mode or\"peer\",master=tostring(masterId or\"\")==id(),masterId=tostring(masterId or\"\"),label=metadata.label,}save(value)remember(peerId,metadata.label,{mode=metadata.mode,masterId=masterId})routes[tostring(peerId)]=nil routesDirty=true audit.log(\"local\",\"peer.rekeyed\",{peer=peerId,mode=metadata.mode})end local function markRejected(reason)if discovery then discovery=nil end setLast(\"cancelled\",reason)end local function discoveryAnnouncement()if not discovery or discovery.state~=\"hosting\"then return false,\"no active host discovery\"end return broadcast({type=\"discover\",session=discovery.id,tag=discovery.tag,label=discovery.label,mode=discovery.mode,master=discovery.masterSide,expires=discovery.expires,})end local function queueConfirmation(peerId,host)confirmations[host.id]=host local sent,err=handshake(peerId,{type=\"pair_confirm\",session=host.id,nonce=host.joinNonce,proof=host.confirmProof,label=host.label,mode=host.mode,master=host.masterId,})host.lastSent,host.retries=now(),0 if not sent then host.error=err end return sent,err end local function expireState(timestamp)if discovery and discovery.expires<=timestamp then local old=discovery.state discovery=nil setLast(\"expired\",old==\"searching\"and\"no matching discovery announcement received\"or\"discovery session expired\")trace(\"discovery\",\"session_expired\",{previous=old})end end local function boundMap(value,limit,timestamp)local entries={}for key,row in pairs(value)do entries[#entries+1]={key=key,at=type(row)==\"number\"and row or(type(row)==\"table\"and(row.expires or row.time or row.seen or row.lastAuthenticated or 0)or 0)}end table.sort(entries,function(a,b)if a.at~=b.at then return a.at<b.at end return tostring(a.key)<tostring(b.key)end)while#entries>limit do value[table.remove(entries,1).key]=nil end end local function pruneRouteBuckets(timestamp)local value=config()local entries={}for peerId,bucket in pairs(routes)do local latest=tonumber(bucket.changedAt)or 0 for _,candidate in ipairs(type(bucket.candidates)==\"table\"and bucket.candidates or{})do latest=math.max(latest,tonumber(candidate.expires)or 0)end entries[#entries+1]={peer=peerId,latest=latest}end table.sort(entries,function(a,b)if a.latest~=b.latest then return a.latest<b.latest end return tostring(a.peer)<tostring(b.peer)end)while#entries>ROUTE_BUCKET_LIMIT do local old=table.remove(entries,1)routes[old.peer],routesDirty=nil,true trace(\"routing\",\"route_bucket_evicted\",{destination=old.peer})end for peerId,state in pairs(linkHealth)do if not activeLink(value,peerId)and not routes[peerId]then linkHealth[peerId]=nil end end for peerId in pairs(advertisedRoutes)do if not activeLink(value,peerId)then advertisedRoutes[peerId]=nil end end boundMap(meshOffers,MESH_STATE_LIMIT,timestamp)boundMap(meshSeen,MESH_STATE_LIMIT,timestamp)end local function controller(value)value=value or config()if value.controller and tostring(value.controller)~=\"\"then local chosen=tostring(value.controller)if chosen==id()or activeLink(value,chosen)then return chosen end local known=routes[chosen]if known then if known.expires and known.expires>now()then return chosen end if type(known.candidates)==\"table\"then for _,candidate in ipairs(known.candidates)do if(candidate.expires or 0)>now()and activeLink(value,candidate.next)then return chosen end end end end trace(\"routing\",\"controller_unreachable\",{controller=chosen})end local selected=tonumber(id())or 0 for peerId in pairs(value.peers)do if activePeer(value,peerId)then local n=tonumber(peerId);if n and n<selected then selected=n end end end for peerId,route in pairs(routes)do local reachable=route.expires and route.expires>now()if type(route.candidates)==\"table\"then reachable=false for _,candidate in ipairs(route.candidates)do if(candidate.expires or 0)>now()then reachable=true;break end end end if reachable then local n=tonumber(peerId);if n and n<selected then selected=n end end end return tostring(selected)end local function meshConfig(value)value=value or config()local mesh=value.mesh if type(mesh)==\"table\"and type(mesh.id)==\"string\"and#mesh.id>=16 and type(mesh.root)==\"string\"and#mesh.root>=32 and tonumber(mesh.epoch)then return mesh end return nil end local function meshMaterial(packet)return canonical({protocol=packet.protocol,type=packet.type,from=packet.from,to=packet.to,mesh=packet.mesh,epoch=packet.epoch,nonce=packet.nonce,offer=packet.offer,timestamp=packet.timestamp})end local function meshTag(mesh,packet)local material=meshMaterial(packet)return material and password.hmacSha256(mesh.root,material)or nil end local function meshPacket(typeName,target,fields)local value,mesh=config(),meshConfig()if not mesh then return false,\"mesh enrollment is pending\"end local packet=copy(fields or{})packet.protocol,packet.type,packet.from,packet.to=PROTOCOL,typeName,id(),target and tostring(target)or 0 packet.mesh,packet.epoch,packet.timestamp=mesh.id,mesh.epoch,now()packet.tag=meshTag(mesh,packet)if not packet.tag then return false,\"could not authenticate mesh packet\"end openChannels(value)local links=wirelessModems()if#links==0 then return false,\"no wireless modem attached\"end local sent,err=transmit(links,value.channel or CHANNEL,packet)if sent then trace(\"routing\",typeName..\"_sent\",{to=packet.to,mesh=mesh.id:sub(1,8)})end return sent,err end local function deriveAutoEdge(mesh,peerId,firstNonce,secondNonce)local left,right=tostring(id()),tostring(peerId)if left>right then left,right=right,left end return password.hmacSha256(mesh.root,\"ceetos/mesh-edge/v1|\"..tostring(mesh.epoch)..\"|\"..left..\"|\"..right..\"|\"..tostring(firstNonce)..\"|\"..tostring(secondNonce))end local function ownsMeshOffer(peerId)local ours,theirs=tonumber(id()),tonumber(peerId)if ours and theirs then return ours<theirs end return tostring(id())<tostring(peerId)end local function installAutoEdge(mesh,peerId,firstNonce,secondNonce,peerLabel)peerId=tostring(peerId)local value=config()if activePeer(value,peerId)then return false,\"manual direct link already exists\"end local key=deriveAutoEdge(mesh,peerId,firstNonce,secondNonce)if not key then return false,\"could not derive mesh edge\"end autoEdges[peerId]={key=key,label=peerLabel,mesh=mesh.id,epoch=mesh.epoch,expires=now()+ROUTE_TTL,firstNonce=firstNonce,secondNonce=secondNonce}local state=healthFor(peerId)state.state,state.successes,state.misses,state.lastAuthenticated,state.stableSince=\"healthy\",2,0,now(),now()routesDirty,meshDirty=true,true trace(\"routing\",\"mesh_edge_active\",{peer=peerId,mesh=mesh.id:sub(1,8)})return true end local function meshEnrollmentAad(from,to,meshId,epoch,nonceCounter)return canonical({protocol=PROTOCOL,type=\"mesh_enroll\",from=tostring(from),to=tostring(to),mesh=meshId,epoch=tonumber(epoch),counter=tonumber(nonceCounter)})end local function sendEnrollment(peerId)local value,mesh,peer=config(),meshConfig(),activePeer(config(),peerId)if not mesh or not peer then return false,\"mesh or direct link unavailable\"end local _,nonceCounter=counter(peerId)local aad=meshEnrollmentAad(id(),peerId,mesh.id,mesh.epoch,nonceCounter)local nonce=meshCrypto.nonce(peer.key,nonceCounter,\"mesh-enroll|\"..id()..\"|\"..tostring(peerId))local plaintext=textutils.serialise({id=mesh.id,epoch=mesh.epoch,root=mesh.root},{compact=true})local envelope,err=meshCrypto.seal(peer.key,nonce,aad,plaintext)if not envelope then return false,err end local sent,sendErr=send(peerId,{type=\"mesh_enroll\",body={mesh=mesh.id,epoch=mesh.epoch,counter=nonceCounter,envelope=envelope},request_id=newRequestId()},peerId)if sent then trace(\"routing\",\"mesh_enrollment_sent\",{to=peerId,epoch=mesh.epoch})end return sent,sendErr end local function acceptEnrollment(from,peer,packet)if not activePeer(config(),from)then return rejectPacket(\"mesh enrollment requires a manual direct link\",packet)end local body=packet.body if type(body)~=\"table\"or type(body.mesh)~=\"string\"or type(body.envelope)~=\"table\"or not tonumber(body.epoch)or not tonumber(body.counter)then return rejectPacket(\"malformed_mesh_enrollment\",packet)end local aad=meshEnrollmentAad(from,id(),body.mesh,body.epoch,body.counter)local plaintext,err=meshCrypto.open(peer.key,body.envelope,aad)if not plaintext then return rejectPacket(err or\"mesh enrollment authentication failed\",packet)end local ok,mesh=pcall(textutils.unserialise,plaintext)if not ok or type(mesh)~=\"table\"or mesh.id~=body.mesh or tonumber(mesh.epoch)~=tonumber(body.epoch)or type(mesh.root)~=\"string\"or#mesh.root<32 then return rejectPacket(\"invalid_mesh_enrollment\",packet)end local value,existing=config(),meshConfig()if existing and existing.id==mesh.id and tonumber(existing.epoch)>tonumber(mesh.epoch)then return end value.mesh={id=mesh.id,epoch=tonumber(mesh.epoch),root=mesh.root,enrolledAt=now(),enrolledBy=tostring(from)}save(value)meshDirty,routesDirty=true,true trace(\"routing\",\"mesh_enrolled\",{from=from,mesh=mesh.id:sub(1,8),epoch=mesh.epoch})end local function ensureMeshEnrollment(timestamp)local value,mesh=config(),meshConfig()if not mesh then if(M._meshBootstrapAt or 0)>timestamp or controller(value)~=id()then return end value.mesh={id=password.newSalt(\"mesh-id:\"..id()),root=password.newSalt(\"mesh-root:\"..id()),epoch=1,createdAt=timestamp,controller=id()}save(value);mesh,meshDirty,routesDirty=value.mesh,true,true trace(\"routing\",\"mesh_created\",{mesh=mesh.id:sub(1,8),epoch=mesh.epoch})end for _,peerId in ipairs(allDirectLinks(value))do if activePeer(value,peerId)then local previous=meshEnrollmentSends[peerId]if not previous or previous.epoch~=mesh.epoch or timestamp-previous.at>=MESH_FULL_INTERVAL then local sent=sendEnrollment(peerId)if sent then meshEnrollmentSends[peerId]={epoch=mesh.epoch,at=timestamp}end end end end end local function meshBeacon(timestamp)local mesh=meshConfig()if not mesh or timestamp-lastMeshBeacon<MESH_BEACON_INTERVAL then return end lastMeshBeacon=timestamp meshPacket(\"mesh_beacon\",nil,{nonce=password.newSalt(\"mesh-beacon:\"..id()),label=label()})end local function verifyMeshPacket(packet)local mesh=meshConfig()if not mesh or type(packet)~=\"table\"or packet.protocol~=PROTOCOL or packet.mesh~=mesh.id or tonumber(packet.epoch)~=tonumber(mesh.epoch)then return false,\"mesh membership proof failed\"end if type(packet.from)~=\"string\"and type(packet.from)~=\"number\"then return false,\"invalid mesh sender\"end if type(packet.nonce)~=\"string\"or#packet.nonce<16 or#packet.nonce>128 then return false,\"invalid mesh nonce\"end if math.abs(now()-(tonumber(packet.timestamp)or 0))>ROUTE_TTL then return false,\"stale mesh packet\"end local material,tag=meshMaterial(packet),packet.tag local expected=material and password.hmacSha256(mesh.root,material)if type(tag)~=\"string\"or not expected or not password.constantTimeEqual(expected,tag)then return false,\"invalid mesh packet tag\"end local replay=tostring(packet.from)..\"|\"..tostring(packet.type)..\"|\"..tostring(packet.nonce)..\"|\"..tostring(packet.offer or\"\")if meshSeen[replay]and meshSeen[replay]+ROUTE_TTL>now()then return false,\"replayed mesh packet\"end meshSeen[replay]=now()return true,mesh end local function handleMeshPacket(packet,distance)local valid,meshOrErr=verifyMeshPacket(packet)if not valid then return rejectPacket(meshOrErr,packet,nil,distance)end local mesh,from=meshOrErr,tostring(packet.from)if from==id()then return end if packet.type==\"mesh_beacon\"then remember(from,packet.label,{mesh=mesh.id})if not activePeer(config(),from)and not autoEdges[from]and ownsMeshOffer(from)then local nonce=password.newSalt(\"mesh-offer:\"..from)meshOffers[from]={offer=nonce,expires=now()+DISCOVERY_TTL}meshPacket(\"mesh_edge_offer\",from,{nonce=nonce,label=label()})end return end if not isForUs(packet.to)then return rejectPacket(\"mesh packet addressed to another peer\",packet,nil,distance)end if packet.type==\"mesh_edge_offer\"then if activePeer(config(),from)then return end local response=password.newSalt(\"mesh-accept:\"..from)installAutoEdge(mesh,from,packet.nonce,response,packet.label)return meshPacket(\"mesh_edge_accept\",from,{nonce=response,offer=packet.nonce,label=label()})end if packet.type==\"mesh_edge_accept\"then local offer=meshOffers[from]if not offer or offer.expires<=now()or offer.offer~=packet.offer then return rejectPacket(\"unexpected mesh edge acceptance\",packet,nil,distance)end meshOffers[from]=nil return installAutoEdge(mesh,from,offer.offer,packet.nonce,packet.label)end end local function probeLinks(timestamp)local value=config()for _,peerId in ipairs(allDirectLinks(value))do local health=healthFor(peerId)local interval=health.state==\"offline\"and PROBE_INTERVAL*3 or PROBE_INTERVAL if timestamp-(health.lastAuthenticated or 0)>=interval and timestamp-(health.lastProbe or 0)>=interval then if health.awaiting then health.misses=(health.misses or 0)+1 health.loss=math.min(1,((health.loss or 0)*3+1)/4)local nextState=health.misses>=PROBE_OFFLINE and\"offline\"or(health.misses>=PROBE_DEGRADED and\"degraded\"or health.state)if nextState~=health.state then health.state,health.successes,health.recoveredAt=nextState,0,nil trace(\"routing\",\"link_\"..nextState,{peer=peerId,misses=health.misses})end routesDirty=true end health.awaiting,health.lastProbe,health.probeToken=true,timestamp,password.newSalt(\"mesh-probe:\"..peerId)send(peerId,{type=\"mesh_probe\",body={token=health.probeToken},request_id=newRequestId()},peerId)end end end local function advertisedMembers(value,peerId,timestamp)local members={}for other,manual in pairs(value.peers)do if other~=peerId and activeLink(value,other)and linkState(other)==\"healthy\"then members[other]={label=manual.label,mode=manual.mode,masterId=manual.masterId,hops=1,kind=\"direct\"}end end for other,edge in pairs(autoEdges)do if other~=peerId and edge.expires>timestamp and linkState(other)==\"healthy\"then members[other]={label=edge.label,mode=\"peer\",hops=1,kind=\"mesh\"}end end for other in pairs(routes)do if other~=peerId and not members[other]then local candidate=primaryCandidate(other,value,false)if candidate and candidate.next~=peerId and linkState(candidate.next)==\"healthy\"then members[other]={label=candidate.label,mode=candidate.mode,masterId=candidate.masterId,hops=candidate.hops,kind=candidate.kind or\"route\"}end end end return members end local function sameAdvertisement(left,right)local leftValue,leftErr=canonical(left)local rightValue,rightErr=canonical(right)return leftValue~=nil and rightValue~=nil and not leftErr and not rightErr and leftValue==rightValue end local function advertiseRoutes(force)local timestamp,value=now(),config()if not force and not routesDirty and timestamp-lastRouteAdvertisement<ROUTE_INTERVAL then return end lastRouteAdvertisement,routesDirty=timestamp,false local full=timestamp-lastMeshFull>=MESH_FULL_INTERVAL if full then lastMeshFull=timestamp end for _,peerId in ipairs(allDirectLinks(value))do if linkUsable(value,peerId,false)then local desired,previous=advertisedMembers(value,peerId,timestamp),advertisedRoutes[peerId]or{}local members,withdrawals={},{}if full then members=desired else for target,meta in pairs(desired)do if not sameAdvertisement(meta,previous[target])then members[target]=meta end end for target in pairs(previous)do if not desired[target]then withdrawals[#withdrawals+1]=target end end end table.sort(withdrawals,function(a,b)return tostring(a)<tostring(b)end)if full or next(members)~=nil or#withdrawals>0 then routeSequence=routeSequence+1 local body={sequence=routeSequence,routeVersion=2,full=full,controller=controller(value),members=members,withdrawals=withdrawals}if send(peerId,{type=\"route_advertise\",body=body,routeSafe=true,request_id=newRequestId()})then advertisedRoutes[peerId]=copy(desired)trace(\"routing\",full and\"route_snapshot_sent\"or\"route_delta_sent\",{to=peerId,changes=(full and 0 or#withdrawals)})end end else advertisedRoutes[peerId]=nil end end end function M.start()migrateV1Links()loadSecurity()assert(meshCrypto.selfTest(),\"CeetOS mesh cryptography self-test failed\")openChannels(config())local status=M.transportStatus()trace(\"transport\",status.wireless and\"wireless_modem_detected\"or\"wireless_modem_missing\",{name=status.name})routesDirty,M._meshBootstrapAt=true,now()end function M.transportStatus()local wireless=wirelessModems()transport.modem,transport.wireless=#modems()>0,#wireless>0 transport.name=wireless[1]and wireless[1].name or nil local out=copy(transport)out.wirelessLinks,out.wirelessLinkCount={},#wireless for _,item in ipairs(wireless)do out.wirelessLinks[#out.wirelessLinks+1]=item.name end out.usesAllWirelessLinks,out.protocol=true,2 out.directPeers,out.manualDirectPeers,out.meshPeers,out.rekeyRequiredPeers,out.routedPeers=0,0,0,0,0 out.healthyLinks,out.degradedLinks,out.offlineLinks=0,0,0 local value=config()for peerId,peer in pairs(value.peers)do if activePeer(value,peerId)then out.directPeers,out.manualDirectPeers=out.directPeers+1,out.manualDirectPeers+1 elseif peer.rekeyRequired then out.rekeyRequiredPeers=out.rekeyRequiredPeers+1 end end for peerId,edge in pairs(autoEdges)do if edge.expires>now()then out.meshPeers,out.directPeers=out.meshPeers+1,out.directPeers+1 end end for peerId,health in pairs(linkHealth)do if health.state==\"healthy\"then out.healthyLinks=out.healthyLinks+1 elseif health.state==\"degraded\"then out.degradedLinks=out.degradedLinks+1 elseif health.state==\"offline\"then out.offlineLinks=out.offlineLinks+1 end end for peerId,bucket in pairs(routes)do local usable=primaryCandidate(peerId,value,false)if usable and not value.peers[peerId]and not autoEdges[peerId]then out.routedPeers=out.routedPeers+1 end end local mesh=meshConfig(value)out.mesh=mesh and{id=mesh.id:sub(1,8),epoch=mesh.epoch,enrolled=true}or{enrolled=false}out.health={healthy=out.healthyLinks,degraded=out.degradedLinks,offline=out.offlineLinks}return out end function M.transportTrace(kind)if kind==\"discovery\"or kind==\"routing\"or kind==\"transport\"then return copy(traces[kind])end return{discovery=copy(traces.discovery),routing=copy(traces.routing),transport=copy(traces.transport)}end function M.discovery()expireState(now())return copy(discovery)end function M.discoveryStatus()expireState(now())local candidate=discovery and discovery.candidate and{id=discovery.candidate.from,label=discovery.candidate.label,mode=discovery.candidate.mode,master=discovery.candidate.master,}or nil local session=discovery and{session=discovery.id,state=discovery.state,mode=discovery.mode,master=discovery.masterSide,expires=discovery.expires,retries=discovery.retries or 0,candidate=candidate and copy(candidate)or nil,}or nil local state=session and session.state or(lastDiscovery and lastDiscovery.state or\"inactive\")return{state=state,active=session~=nil,lastState=lastDiscovery and lastDiscovery.state or\"inactive\",lastMessage=lastDiscovery and lastDiscovery.message or nil,session=session,searching=session and session.state==\"searching\"or false,candidate=candidate,discovered=#M.seenPeers(),transport=M.transportStatus(),message=(discovery and discovery.error)or(lastDiscovery and lastDiscovery.message),}end function M.startDiscovery(mode,masterSide,phrase)if activeSession()then return false,\"cancel the active discovery session first\"end local normalized,phraseErr=M.normalizePhrase(phrase)if not normalized then return false,phraseErr end mode=mode==\"master\"and\"master\"or\"peer\"masterSide=masterSide==\"joiner\"and\"joiner\"or\"initiator\"local sessionId=password.newSalt(\"discovery:\"..id())discovery={id=sessionId,state=\"hosting\",phrase=normalized,tag=phraseTag(normalized,sessionId,id()),mode=mode,masterSide=masterSide,label=label(),initiator=id(),expires=now()+DISCOVERY_TTL,retries=0,lastBroadcast=0,}local sent,err=discoveryAnnouncement()if not sent then discovery=nil;setLast(\"cancelled\",err);return false,err end discovery.lastBroadcast=now()setLast(\"hosting\")return true,{state=\"hosting\",session=sessionId,expires=discovery.expires}end function M.joinDiscovery(phrase)if activeSession()then return false,\"cancel the active discovery session first\"end local normalized,phraseErr=M.normalizePhrase(phrase)if not normalized then return false,phraseErr end local candidate for _,item in pairs(discovered)do if item.expires>now()and password.constantTimeEqual(item.tag or\"\",phraseTag(normalized,item.session,item.from))then candidate=item;break end end discovery={state=\"searching\",phrase=normalized,expires=now()+DISCOVERY_TTL,retries=0,lastQuery=0}if candidate then discovery.state,discovery.candidate=\"candidate\",candidate return true,{state=\"candidate\",candidate={id=candidate.from,label=candidate.label,mode=candidate.mode,master=candidate.master}}end local sent,err=broadcast({type=\"discover_query\"})if not sent then discovery=nil;setLast(\"cancelled\",err);return false,err end discovery.lastQuery=now()setLast(\"searching\",\"waiting for matching discovery announcement\")return true,{state=\"searching\"}end function M.confirmDiscovery()if not discovery or discovery.state~=\"candidate\"or not discovery.candidate then return false,\"no discovery candidate is awaiting confirmation\"end local candidate,value=discovery.candidate,config()local existing=value.peers[tostring(candidate.from)]if existing and not existing.rekeyRequired then return false,\"computer is already a network member\"end if routes[tostring(candidate.from)]and not existing then return false,\"computer is already a routed network member\"end local nonce=password.newSalt(\"pair-join:\"..candidate.session)local proof=joinProof(discovery.phrase,candidate.session,candidate.from,id(),nonce)discovery.state,discovery.joinNonce,discovery.joinProof,discovery.lastSent,discovery.retries=\"confirming\",nonce,proof,now(),0 local sent,err=handshake(candidate.from,{type=\"pair_join\",session=candidate.session,nonce=nonce,proof=proof,label=label()})if not sent then discovery.error=err;return false,err end trace(\"discovery\",\"pair_confirmation_requested\",{peer=candidate.from})return true end function M.cancelDiscovery()if discovery and discovery.state==\"hosting\"then broadcast({type=\"discover_cancel\",session=discovery.id})end discovery=nil setLast(\"cancelled\",\"discovery cancelled\")trace(\"discovery\",\"session_cancelled\")return true end function M.normalizePhrase(value)if type(value)~=\"string\"then return nil,\"phrase is required\"end if value:find(\"[%z\\1-\\31\\127]\")then return nil,\"phrase contains control characters\"end value=value:gsub(\"^%s+\",\"\"):gsub(\"%s+$\",\"\")if#value<8 then return nil,\"phrase must be at least 8 characters\"end if#value>128 then return nil,\"phrase must be at most 128 characters\"end return value end function M.tick()local timestamp=now()expireState(timestamp)if discovery then if discovery.state==\"hosting\"and timestamp-(discovery.lastBroadcast or 0)>=math.min(30000,DISCOVERY_INTERVAL*(2^math.min(discovery.retries or 0,4)))then local sent,err=discoveryAnnouncement()discovery.lastBroadcast=timestamp discovery.retries=(discovery.retries or 0)+1 if not sent then discovery.error=err end elseif discovery.state==\"searching\"and timestamp-(discovery.lastQuery or 0)>=DISCOVERY_INTERVAL then local sent,err=broadcast({type=\"discover_query\"})discovery.lastQuery,discovery.retries=timestamp,(discovery.retries or 0)+1 if not sent then discovery.error=err end elseif discovery.state==\"confirming\"and timestamp-(discovery.lastSent or 0)>=PAIR_RETRY then local candidate=discovery.candidate local sent,err=handshake(candidate.from,{type=\"pair_join\",session=candidate.session,nonce=discovery.joinNonce,proof=discovery.joinProof,label=label()})discovery.lastSent,discovery.retries=timestamp,(discovery.retries or 0)+1 if not sent then discovery.error=err end end end for sessionId,item in pairs(discovered)do if item.expires<=timestamp then discovered[sessionId]=nil end end for sessionId,record in pairs(confirmations)do if record.expires<=timestamp then confirmations[sessionId]=nil elseif timestamp-(record.lastSent or 0)>=PAIR_RETRY then queueConfirmation(record.peer,record)end end for peerId,bucket in pairs(routes)do local kept={}for _,candidate in ipairs(routeBucket(peerId).candidates)do if candidate.expires>timestamp then kept[#kept+1]=candidate end end bucket.candidates=kept if#kept==0 and not activeLink(config(),peerId)then routes[peerId],routesDirty=nil,true end end for peerId,edge in pairs(autoEdges)do if edge.expires<=timestamp then autoEdges[peerId],routesDirty=nil,true;trace(\"routing\",\"mesh_edge_expired\",{peer=peerId})end end for key,seenAt in pairs(meshSeen)do if seenAt+ROUTE_TTL<=timestamp then meshSeen[key]=nil end end for peerId,offer in pairs(meshOffers)do if offer.expires<=timestamp then meshOffers[peerId]=nil end end for requestId,seenAt in pairs(seenPackets)do if seenAt+ROUTE_TTL<=timestamp then seenPackets[requestId]=nil end end for requestId,route in pairs(reverseRoutes)do if route.expires<=timestamp then reverseRoutes[requestId]=nil end end boundMap(meshSeen,MESH_STATE_LIMIT,timestamp)boundMap(meshOffers,MESH_STATE_LIMIT,timestamp)pruneRouteBuckets(timestamp)ensureMeshEnrollment(timestamp)meshBeacon(timestamp)probeLinks(timestamp)advertiseRoutes(false)end function M.peers()local out,value,seen={},config(),read(SEEN_PATH,{})for peerId,peer in pairs(value.peers)do out[#out+1]={id=peerId,label=peer.label or(seen[peerId]and seen[peerId].label),role=peer.role or\"operator\",mode=peer.mode or\"peer\",master=peer.master==true,masterId=peer.masterId,direct=true,active=activePeer(value,peerId)~=nil,rekeyRequired=peer.rekeyRequired==true or peer.protocol~=2,health=linkState(peerId),auto=false,}end for peerId,edge in pairs(autoEdges)do if not value.peers[peerId]and edge.expires>now()then out[#out+1]={id=peerId,label=edge.label or(seen[peerId]and seen[peerId].label),role=\"mesh\",mode=\"peer\",direct=true,auto=true,active=true,health=linkState(peerId)}end end for peerId in pairs(routes)do if not value.peers[peerId]and not autoEdges[peerId]then local route=primaryCandidate(peerId,value,false)if route then out[#out+1]={id=peerId,label=route.label or(seen[peerId]and seen[peerId].label),role=\"routed\",mode=route.mode or\"peer\",masterId=route.masterId,routed=true,hops=route.hops,next=route.next,health=linkState(route.next),alternates=#routeBucket(peerId).candidates-1}end end end table.sort(out,function(a,b)return tonumber(a.id)<tonumber(b.id)end)return out end function M.directPeers()local out={}for _,peer in ipairs(M.peers())do if peer.direct and peer.active then out[#out+1]=peer end end return out end function M.seenPeers()local out={}for peerId,item in pairs(read(SEEN_PATH,{}))do if not item.seen or item.seen+DISCOVERY_TTL*2>now()then out[#out+1]={id=peerId,label=item.label,seen=item.seen,session=item.session}end end table.sort(out,function(a,b)return tonumber(a.id)<tonumber(b.id)end)return out end function M.routeStatus(target)local value,out=config(),{}local function row(peerId)local selected=primaryCandidate(peerId,value,false)if not selected then return end local health=healthFor(selected.next)local alternates={}for _,candidate in ipairs(validCandidates(peerId,value))do if tostring(candidate.next)~=tostring(selected.next)then local state=healthFor(candidate.next)alternates[#alternates+1]={next=candidate.next,hops=candidate.hops,kind=candidate.kind or\"route\",health=state.state,loss=state.loss or 0,rtt=state.rtt,distance=state.lastDistance}end end table.sort(alternates,compareCandidates)while#alternates>ROUTE_CANDIDATE_LIMIT-1 do table.remove(alternates)end out[#out+1]={id=tostring(peerId),label=selected.label,next=selected.next,hops=selected.hops,kind=selected.kind or\"route\",health=health.state,loss=health.loss or 0,rtt=health.rtt,distance=health.lastDistance,selectedAt=routeBucket(peerId).changedAt,reason=routeBucket(peerId).reason,alternates=alternates,}end if target then row(tostring(target))else local ids,seen={},{}for peerId in pairs(value.peers)do ids[#ids+1],seen[peerId]=peerId,true end for peerId in pairs(autoEdges)do if not seen[peerId]then ids[#ids+1],seen[peerId]=peerId,true end end for peerId in pairs(routes)do if not seen[peerId]then ids[#ids+1],seen[peerId]=peerId,true end end table.sort(ids,function(a,b)return tonumber(a)<tonumber(b)end)for _,peerId in ipairs(ids)do row(peerId)end end return out end function M.statusSnapshot()return{protocol=2,transport=M.transportStatus(),discovery=M.discoveryStatus(),peers=M.peers(),routes=M.routeStatus(),trace=M.transportTrace(),controller=M.controller()}end function M.controller()return controller()end function M.isDirectController(from,packet)return tostring(from)==tostring(controller())and type(packet)==\"table\"and tostring(packet.origin or\"\")==tostring(from)end function M.setController(value)assert(tonumber(value),\"controller must be a computer ID\")local saved=config();saved.controller=tostring(value);save(saved);routesDirty=true audit.log(\"local\",\"jobs.controller\",{controller=saved.controller})return saved.controller end function M.request(target,typeName,body,options)if type(typeName)~=\"string\"or#typeName==0 or#typeName>64 then return false,\"invalid request type\"end local destination=tostring(target)local value=config()if not primaryCandidate(destination,value,options and options.safe==true)then return false,\"peer is unreachable, degraded, or requires rekey\"end return send(destination,{type=typeName,body=body or{},origin=id(),routeSafe=options and options.safe==true,balanceSafe=options and options.safe==true,confidential=options and options.confidential==true,request_id=newRequestId()})end function M.reply(request,typeName,body,options)if type(request)~=\"table\"or type(typeName)~=\"string\"or#typeName==0 or#typeName>64 then return false,\"invalid reply\"end local requestId,destination=request.request_id,tostring(request.origin or\"\")local reverse=type(requestId)==\"string\"and reverseRoutes[requestId]or nil if destination==\"\"or not reverse or reverse.expires<=now()or reverse.origin~=destination then return false,\"reverse reply route is unavailable\"end return send(destination,{type=typeName,body=body or{},origin=id(),destination=destination,reply_to=requestId,routeSafe=options and options.safe==true,confidential=options and options.confidential==true,request_id=newRequestId(),},reverse.previous)end function M.offer()return false,\"legacy pairing is disabled; use phrase discovery\"end function M.accept()return false,\"legacy pairing is disabled; use phrase discovery\"end function M.discover()return broadcast({type=\"discover_query\"})end rejectPacket=function(reason,packet,channel,distance)transport.rejected=transport.rejected+1 trace(\"transport\",\"packet_rejected\",{reason=reason,type=type(packet)==\"table\"and packet.type or nil,channel=channel,distance=distance,from=type(packet)==\"table\"and packet.from or nil})end local function handleDiscovery(packet,channel,distance)local from=tostring(packet.from)if packet.type==\"discover_cancel\"and isBroadcast(packet.to)then discovered[tostring(packet.session)]=nil trace(\"discovery\",\"cancel_received\",{from=from,distance=distance})return end if packet.type==\"discover_query\"and isBroadcast(packet.to)and discovery and discovery.state==\"hosting\"then discoveryAnnouncement()trace(\"discovery\",\"query_answered\",{from=from,distance=distance})return end if packet.type==\"discover\"and isBroadcast(packet.to)and from~=id()then if type(packet.session)~=\"string\"or#packet.session<16 or type(packet.tag)~=\"string\"or#packet.tag~=64 then return rejectPacket(\"malformed_discovery\",packet,channel,distance)end local expires=tonumber(packet.expires)or(now()+DISCOVERY_TTL)discovered[packet.session]={session=packet.session,from=from,tag=packet.tag,label=tostring(packet.label or(\"Computer \"..from)):sub(1,128),mode=packet.mode==\"master\"and\"master\"or\"peer\",master=packet.master==\"joiner\"and\"joiner\"or\"initiator\",expires=math.min(expires,now()+DISCOVERY_TTL)}remember(from,packet.label,{session=packet.session})trace(\"discovery\",\"announcement_received\",{from=from,label=packet.label,distance=distance})if discovery and discovery.state==\"searching\"and password.constantTimeEqual(packet.tag,phraseTag(discovery.phrase,packet.session,from))then local value,existing=config(),config().peers[from]if existing and not existing.rekeyRequired then markRejected(\"computer is already a direct peer\")elseif routes[from]and not existing then markRejected(\"computer is already a routed network member\")else discovery.state,discovery.candidate,discovery.error=\"candidate\",discovered[packet.session],nil setLast(\"candidate\",\"matching computer found; confirm pairing\")trace(\"discovery\",\"candidate_found\",{from=from,label=packet.label,distance=distance})end end return end end local function handlePairJoin(packet,distance)if not discovery or discovery.state~=\"hosting\"or packet.session~=discovery.id then return end local from=tostring(packet.from)local value,existing=config(),config().peers[from]if(existing and not existing.rekeyRequired)or(routes[from]and not existing)then handshake(from,{type=\"pair_reject\",session=packet.session,reason=\"already_network_member\"})trace(\"discovery\",\"pair_rejected\",{from=from,reason=\"already_network_member\"})return end if type(packet.nonce)~=\"string\"or#packet.nonce<16 or#packet.nonce>128 or type(packet.proof)~=\"string\"or#packet.proof~=64 then return rejectPacket(\"malformed_pair_join\",packet,nil,distance)end local expected=joinProof(discovery.phrase,discovery.id,id(),from,packet.nonce)if not password.constantTimeEqual(expected,packet.proof)then discovery.failures=(discovery.failures or 0)+1 if discovery.failures>=5 then markRejected(\"too many invalid pairing proofs\")end handshake(from,{type=\"pair_reject\",session=packet.session,reason=\"phrase_mismatch\"})trace(\"discovery\",\"pair_rejected\",{from=from,reason=\"phrase_mismatch\"})return end local masterId=discovery.mode==\"master\"and(discovery.masterSide==\"initiator\"and id()or from)or\"\"local key,keyErr=linkKey(discovery.phrase,discovery.id,id(),from,packet.nonce)if not key then return markRejected(keyErr)end persistPair(from,key,{mode=discovery.mode,label=packet.label},masterId)local record={id=discovery.id,peer=from,joinNonce=packet.nonce,mode=discovery.mode,masterId=masterId,label=label(),expires=now()+DISCOVERY_TTL}record.confirmProof=confirmProof(discovery.phrase,record.id,id(),from,record.joinNonce,record.mode,record.masterId)discovery=nil setLast(\"paired\",\"awaiting pairing acknowledgement\")trace(\"discovery\",\"pair_join_verified\",{from=from,distance=distance})return queueConfirmation(from,record)end local function handlePairConfirm(packet,distance)if not discovery or discovery.state~=\"confirming\"or not discovery.candidate or packet.session~=discovery.candidate.session then return end local from=tostring(packet.from)if from~=tostring(discovery.candidate.from)or packet.nonce~=discovery.joinNonce then return rejectPacket(\"unexpected_pair_confirmation\",packet,nil,distance)end local expected=confirmProof(discovery.phrase,packet.session,from,id(),discovery.joinNonce,packet.mode==\"master\"and\"master\"or\"peer\",packet.master or\"\")if not password.constantTimeEqual(expected,packet.proof or\"\")then return markRejected(\"pair confirmation proof did not match\")end local key,err=linkKey(discovery.phrase,packet.session,from,id(),discovery.joinNonce)if not key then return markRejected(err)end persistPair(from,key,{mode=packet.mode,label=packet.label},packet.master)local sessionId=packet.session discovery=nil setLast(\"paired\",\"paired with \"..tostring(packet.label or from))handshake(from,{type=\"pair_ack\",session=sessionId,proof=password.hmacSha256(key,\"ceetos/pair/ack/v2|\"..sessionId)})trace(\"discovery\",\"pair_confirmed\",{from=from,distance=distance})return true end local function handlePairAck(packet)local record=confirmations[packet.session]if not record or tostring(record.peer)~=tostring(packet.from)then return end local value,peer=config(),activePeer(config(),record.peer)if not peer then return end local expected=password.hmacSha256(peer.key,\"ceetos/pair/ack/v2|\"..packet.session)if password.constantTimeEqual(expected,packet.proof or\"\")then confirmations[packet.session]=nil setLast(\"paired\",\"paired with \"..tostring(record.peer))routesDirty=true trace(\"discovery\",\"pair_acknowledged\",{from=packet.from})end end local function handleRouteAdvertisement(from,packet)local body=packet.body if type(body)~=\"table\"or type(body.sequence)~=\"number\"or type(body.members)~=\"table\"or(body.withdrawals~=nil and type(body.withdrawals)~=\"table\")then return rejectPacket(\"malformed_route_advertisement\",packet)end local key=tostring(from)M._routeSequences=M._routeSequences or{}if(M._routeSequences[key]or-1)>=body.sequence then return end M._routeSequences[key]=body.sequence local value,changed,count=config(),false,0 if body.controller and not value.controller then value.controller,changed=tostring(body.controller),true end local announced={}for advertisedId,meta in pairs(body.members)do count=count+1 if count>MAX_PACKET_TABLE then return rejectPacket(\"route_advertisement_too_large\",packet)end local peerId=tostring(advertisedId)announced[peerId]=true if peerId~=id()and peerId~=key and type(meta)==\"table\"then local hops=math.floor(tonumber(meta.hops)or 0)+1 if hops>1 and hops<=PACKET_TTL and type(meta.label)~=\"table\"and type(meta.mode)~=\"table\"then local bucket,found=routeBucket(peerId),nil for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)==key then found=candidate;break end end if found then local metadataChanged=found.hops~=hops or found.label~=meta.label or found.mode~=meta.mode or tostring(found.masterId or\"\")~=tostring(meta.masterId or\"\")or found.kind~=(meta.kind or\"route\")found.hops,found.expires=hops,now()+ROUTE_TTL found.label,found.mode,found.masterId,found.kind=meta.label,meta.mode,meta.masterId,meta.kind or\"route\"if metadataChanged then changed=true end else bucket.candidates[#bucket.candidates+1]={next=key,hops=hops,expires=now()+ROUTE_TTL,label=type(meta.label)==\"string\"and meta.label:sub(1,128)or nil,mode=meta.mode==\"master\"and\"master\"or\"peer\",masterId=meta.masterId and tostring(meta.masterId)or nil,kind=meta.kind==\"mesh\"and\"mesh\"or\"route\"}table.sort(bucket.candidates,compareCandidates)while#bucket.candidates>ROUTE_CANDIDATE_LIMIT do table.remove(bucket.candidates)end changed=true end end end end local withdrawals=body.withdrawals or{}for _,withdrawnId in pairs(withdrawals)do count=count+1 if count>MAX_PACKET_TABLE then return rejectPacket(\"route_advertisement_too_large\",packet)end local bucket=routes[tostring(withdrawnId)]if bucket and type(bucket.candidates)==\"table\"then local kept={}for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)~=key then kept[#kept+1]=candidate else changed=true end end bucket.candidates=kept end end if body.full==true then for peerId,bucket in pairs(routes)do if type(bucket.candidates)==\"table\"and not announced[tostring(peerId)]then local kept={}for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)~=key then kept[#kept+1]=candidate else changed=true end end bucket.candidates=kept end end end if changed then if value.controller then save(value)end;routesDirty=true;trace(\"routing\",\"route_table_updated\",{from=from})end end local function handleProbe(from,packet)local token=type(packet.body)==\"table\"and packet.body.token if type(token)~=\"string\"or#token<16 or#token>128 then return rejectPacket(\"malformed link probe\",packet)end touchLink(from,nil,\"probe\")return send(from,{type=\"mesh_probe_ack\",body={token=token},request_id=newRequestId()},from)end local function handleProbeAck(from,packet)local health,token=healthFor(from),type(packet.body)==\"table\"and packet.body.token if not health.awaiting or token~=health.probeToken then return rejectPacket(\"unexpected link probe acknowledgement\",packet)end local sample=math.max(0,now()-(health.lastProbe or now()))health.rtt=health.rtt and math.floor((health.rtt*3+sample)/4)or sample health.awaiting,health.misses,health.probeToken=false,0,nil touchLink(from,health.lastDistance,\"probe_ack\")end function M.handle(event)if event[1]~=\"modem_message\"then return end local channel,packet,distance=event[3],event[5],event[6]local expected=false for _,item in ipairs(transport.channels or{})do if channel==item then expected=true;break end end if not expected then return rejectPacket(\"unexpected_channel\",packet,channel,distance)end transport.lastReceive,transport.lastDistance=now(),distance if type(packet)~=\"table\"then return rejectPacket(\"packet_not_table\",packet,channel,distance)end if packet.protocol==PUBLIC_UPDATE_PROTOCOL then if channel~=DISCOVERY_CHANNEL or not PUBLIC_UPDATE_TYPES[packet.type]or type(packet.from)~=\"string\"and type(packet.from)~=\"number\"or not(isForUs(packet.to)or isBroadcast(packet.to))then return rejectPacket(\"invalid_public_update_packet\",packet,channel,distance)end local encoded,encodeErr=canonical(packet.body or{})if not encoded then return rejectPacket(\"invalid_public_update_body:\"..tostring(encodeErr),packet,channel,distance)end trace(\"transport\",\"public_update_received\",{type=packet.type,from=packet.from,distance=distance,bytes=#encoded})if M.publicHandler then return M.publicHandler(packet,distance)end return end local discoveryPacket=packet.type==\"discover\"or packet.type==\"discover_query\"or packet.type==\"discover_cancel\"local handshakePacket=packet.type==\"pair_join\"or packet.type==\"pair_confirm\"or packet.type==\"pair_ack\"or packet.type==\"pair_reject\"local meshPacketType=packet.type==\"mesh_beacon\"or packet.type==\"mesh_edge_offer\"or packet.type==\"mesh_edge_accept\"if discoveryPacket then if packet.protocol~=PROTOCOL or channel~=DISCOVERY_CHANNEL then return rejectPacket(\"invalid_discovery_transport\",packet,channel,distance)end return handleDiscovery(packet,channel,distance)end if handshakePacket then if not isForUs(packet.to)then return end if packet.protocol~=PROTOCOL or channel~=(config().channel or CHANNEL)then return rejectPacket(\"invalid_handshake_transport\",packet,channel,distance)end if packet.type==\"pair_join\"then return handlePairJoin(packet,distance)elseif packet.type==\"pair_confirm\"then return handlePairConfirm(packet,distance)elseif packet.type==\"pair_ack\"then return handlePairAck(packet)elseif packet.type==\"pair_reject\"and discovery then markRejected(\"pairing rejected: \"..tostring(packet.reason or\"remote peer rejected it\"));return end return end if meshPacketType then if packet.protocol~=PROTOCOL or channel~=(config().channel or CHANNEL)then return rejectPacket(\"invalid mesh transport\",packet,channel,distance)end if packet.type~=\"mesh_beacon\"and not isForUs(packet.to)then return end return handleMeshPacket(packet,distance)end if not isForUs(packet.to)then return end local value=config()local valid,peerOrReason=verifyPacket(packet,value)if not valid then return rejectPacket(peerOrReason,packet,channel,distance)end local peer,from=peerOrReason,tostring(packet.from)if packet.confidential==true then local opened,openErr=openBody(peer,packet)if not opened then return rejectPacket(openErr or\"could not open confidential packet\",packet,channel,distance)end packet.body=opened end touchLink(from,distance,\"authenticated_packet\")if autoEdges[from]then autoEdges[from].expires=now()+ROUTE_TTL end trace(\"routing\",\"packet_received\",{type=packet.type,from=from,destination=packet.destination,distance=distance})if packet.type==\"mesh_enroll\"then return acceptEnrollment(from,peer,packet)end if packet.type==\"mesh_probe\"then return handleProbe(from,packet)end if packet.type==\"mesh_probe_ack\"then return handleProbeAck(from,packet)end if packet.type==\"route_advertise\"then return handleRouteAdvertisement(from,packet)end if packet.reply_to and packet.destination and tostring(packet.destination)~=id()then local reverse=reverseRoutes[packet.reply_to]if not reverse or reverse.expires<=now()or reverse.origin~=tostring(packet.destination)then return rejectPacket(\"reverse reply route unavailable\",packet,channel,distance)end local requestId=tostring(packet.request_id or\"\")if seenPackets[requestId]then return trace(\"routing\",\"forward_duplicate\",{from=from,type=packet.type})end seenPackets[requestId]=now()if(tonumber(packet.ttl)or 0)<=1 then return rejectPacket(\"TTL exhausted\",packet,channel,distance)end packet.ttl=packet.ttl-1 trace(\"routing\",\"reverse_reply_forwarded\",{from=from,to=reverse.previous,type=packet.type})return send(packet.destination,packet,reverse.previous)end if packet.destination and tostring(packet.destination)~=id()then local requestId=tostring(packet.request_id or\"\")if requestId==\"\"or seenPackets[requestId]then return trace(\"routing\",\"forward_duplicate\",{from=from,type=packet.type})end seenPackets[requestId]=now()local remembered,rememberErr=rememberReverse(packet,from)if not remembered then return rejectPacket(rememberErr or\"could not record reverse route\",packet,channel,distance)end if(tonumber(packet.ttl)or 0)<=1 then return rejectPacket(\"TTL exhausted\",packet,channel,distance)end packet.ttl=packet.ttl-1 return send(packet.destination,packet)end local requestId=tostring(packet.request_id or\"\")if requestId==\"\"then return rejectPacket(\"missing request ID\",packet,channel,distance)end if seenPackets[requestId]then return trace(\"routing\",\"destination_duplicate\",{from=from,type=packet.type})end seenPackets[requestId]=now()local remembered,rememberErr=rememberReverse(packet,from)if not remembered then return rejectPacket(rememberErr or\"could not record reverse route\",packet,channel,distance)end packet.authenticatedHop,packet.originVerified=from,false if M.handler then return M.handler(from,peer,packet)end end return M",
  ["ceetos/lib/network_ipc.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local ROOT=\"/ceetos/data/network-ipc\"local COMMANDS=ROOT..\"/commands\"local RESULTS=ROOT..\"/results\"local function ensure(path)if not fs.exists(path)then fs.makeDir(path)end end local function safeId(value)return type(value)==\"string\"and value:match(\"^[%w_%-%.]+$\")and#value<=160 end local function path(directory,requestId)return fs.combine(directory,requestId..\".lua\")end function M.submit(request)if type(request)~=\"table\"or not safeId(request.id)then return false,\"invalid IPC request ID\"end ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local target=path(COMMANDS,request.id)if fs.exists(target)then return false,\"IPC request already exists\"end return store.write(target,request)end function M.take()ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local names=fs.list(COMMANDS)table.sort(names)for _,name in ipairs(names)do local requestId=name:match(\"^([%w_%-%.]+)%.lua$\")if requestId and safeId(requestId)then local target=fs.combine(COMMANDS,name)store.invalidate(target)local request=store.readFresh(target,nil)fs.delete(target)if type(request)==\"table\"and request.id==requestId then return request end end end end function M.reply(requestId,value)if not safeId(requestId)or type(value)~=\"table\"then return false,\"invalid IPC result\"end ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)return store.write(path(RESULTS,requestId),value)end function M.poll(requestId,consume)if not safeId(requestId)then return nil,\"invalid IPC request ID\"end local target=path(RESULTS,requestId)if not fs.exists(target)then return nil end local handle=fs.open(target,\"r\")if not handle then return nil end local raw=handle.readAll()handle.close()local ok,result=pcall(textutils.unserialise,raw)if not ok or type(result)~=\"table\"or result.id~=requestId then return nil,\"invalid IPC result\"end if consume then fs.delete(target)end return result end function M.prune(limit)ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)limit=math.max(1,math.floor(tonumber(limit)or 32))local names={}for _,name in ipairs(fs.list(RESULTS))do if name:match(\"^[%w_%-%.]+%.lua$\")then names[#names+1]=name end end table.sort(names)while#names>limit do fs.delete(fs.combine(RESULTS,table.remove(names,1)))end end function M.status()ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local function count(directory)local total=0 for _,name in ipairs(fs.list(directory))do if name:match(\"^[%w_%-%.]+%.lua$\")then total=total+1 end end return total end return{commands=count(COMMANDS),results=count(RESULTS)}end return M",
  ["ceetos/lib/auth_client.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local networkIpc=require(\"ceetos.lib.network_ipc\")local M={}local REPLIES=\"/ceetos/data/auth-replies.lua\"local sequence=0 local function now()return os.epoch(\"utc\")end local function authority()store.invalidate(\"/ceetos/data/auth-authority.lua\")local value=store.read(\"/ceetos/data/auth-authority.lua\",nil)return type(value)==\"table\"and value.active==true and value or nil end local function requestId(kind)sequence=sequence+1 return table.concat({\"auth\",kind,tostring(os.getComputerID()),tostring(now()),tostring(sequence)},\"-\")end local function take(id)store.invalidate(REPLIES)local replies=store.read(REPLIES,{})local row=replies[id]if row then replies[id]=nil;store.write(REPLIES,replies)end return row end function M.receive(body)if type(body)~=\"table\"or type(body.request_id)~=\"string\"or#body.request_id>160 then return false,\"invalid authority reply\"end local replies=store.read(REPLIES,{})replies[body.request_id]={ok=body.ok==true,result=body.result,error=body.error,at=now()}local rows={}for id,row in pairs(replies)do rows[#rows+1]={id=id,at=tonumber(row.at)or 0}end table.sort(rows,function(a,b)return a.at>b.at end)while#rows>32 do replies[table.remove(rows).id]=nil end return store.write(REPLIES,replies)end local function send(typeName,body,timeout)local server=authority()if not server then return nil,\"no Auth Server is reachable\"end local id=body.request_id or requestId(typeName)body.request_id=id local command={id=\"auth-\"..id,action=\"auth_send\",target=server.id,type=typeName,body=body}local ok,err=networkIpc.submit(command)if not ok then return nil,err or\"could not reach CeetOS network service\"end local deadline=now()+(timeout or 6000)while now()<deadline do local ack=networkIpc.poll(command.id,true)if ack and ack.ok~=true then return nil,ack.error or\"Auth Server request was not sent\"end local reply=take(id)if reply then return reply.ok and reply.result or nil,reply.error or\"Auth Server rejected request\"end sleep(0.05)end return nil,\"Auth Server did not respond\"end local function loginProof(verifier,server,request,challenge,expires)return password.hmacSha256(verifier,table.concat({\"ceetos/auth/login/v1\",tostring(server),tostring(os.getComputerID()),tostring(request),tostring(challenge),tostring(expires)},\"|\"))end function M.login(name,plainPassword)if type(name)~=\"string\"or type(plainPassword)~=\"string\"then return false,\"invalid credentials\"end local cloud=require(\"ceetos.lib.cloud_sync\")if cloud.authOnline()then return cloud.login(name,plainPassword)end local request=requestId(\"begin\")local begin,beginErr=send(\"auth_login_begin\",{request_id=request,username=name,node=tostring(os.getComputerID())})if not begin then return false,beginErr end local verifier,deriveErr=password.derive(plainPassword,begin.salt,begin.workFactor)if not verifier then return false,deriveErr end local proof=loginProof(verifier,begin.authority,request,begin.challenge,begin.expires)local result,proofErr=send(\"auth_login_proof\",{request_id=request,username=name,node=tostring(os.getComputerID()),challenge=begin.challenge,proof=proof})if not result then return false,proofErr end local auth=require((\"ceetos.lib.auth\"))local saved,saveErr=auth.saveCentralSession(result.session)if not saved then return false,saveErr end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end function M.mutate(action,fields)local auth=require((\"ceetos.lib.auth\"))local cloud=require(\"ceetos.lib.cloud_sync\")if cloud.authOnline()then return cloud.mutate(action,fields)end local current,server=auth.refreshSession(),authority()if not current or not current.central or not server then return false,\"an Auth Server login is required\"end local result,err=send(\"auth_mutate\",{action=action,fields=fields,session=current.nonce,node=tostring(os.getComputerID()),revision=current.accountRevision})if not result then return false,err end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end return M",
  ["ceetos/lib/jobs.lua"] = "local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local M={}local PATH,WORKERS_PATH=\"/ceetos/data/jobs.lua\",\"/ceetos/data/job-workers.lua\"local HISTORY_LIMIT,OUTPUT_LIMIT=48,4096 local HEARTBEAT_TIMEOUT,DEFAULT_CAPACITY=15000,1 local DEFAULT_TIMEOUT,DEFAULT_ATTEMPTS=60000,2 local context=nil local function now()if context and context.now then return context.now()end return os.epoch(\"utc\")end local function localId()if context and(context.id~=nil or context.nodeId~=nil)then return tostring(context.id or context.nodeId)end return tostring(os.getComputerID())end local function backend()return context and context.store or store end local function paths()return context and context.path or PATH,context and context.workersPath or WORKERS_PATH end local function log(action,detail)if not(context and context.noAudit~=false)then audit.log(\"system\",action,detail)end end local function copy(value,seen)if type(value)~=\"table\"then return value end seen=seen or{}if seen[value]then return seen[value]end local result={};seen[value]=result for key,item in pairs(value)do result[copy(key,seen)]=copy(item,seen)end return result end local function clampNumber(value,low,high,fallback)value=tonumber(value)if not value then return fallback end return math.max(low,math.min(high,math.floor(value)))end local function cleanOutput(value)value=tostring(value or\"\")if#value>OUTPUT_LIMIT then return value:sub(1,OUTPUT_LIMIT)..\"\\n[output truncated]\"end return value end local function defaultState()return{controller=(context and context.controllerId and tostring(context.controllerId))or localId(),capacity=DEFAULT_CAPACITY,workers={},templates={},queue={},running={},schedules={},history={},sequence=0,fair=0,}end local function normalise(data)data=type(data)==\"table\"and data or defaultState()local defaults=defaultState()for key,value in pairs(defaults)do if data[key]==nil then data[key]=value end end data.controller=tostring(data.controller or localId())data.capacity=clampNumber(data.capacity,1,32,DEFAULT_CAPACITY)data.workers,data.templates,data.queue=data.workers or{},data.templates or{},data.queue or{}data.running,data.schedules,data.history=data.running or{},data.schedules or{},data.history or{}data.sequence,data.fair=tonumber(data.sequence)or 0,tonumber(data.fair)or 0 return data end local function load()local mainPath,workersPath=paths()local disk=backend()if disk.invalidate then disk.invalidate(mainPath);disk.invalidate(workersPath)end local data=normalise(disk.read(mainPath,defaultState()))data.workers=disk.read(workersPath,data.workers or{})or{}return normalise(data)end local function save(data)local mainPath,workersPath=paths()local disk=backend()local workers=data.workers or{}local main=copy(data);main.workers=nil disk.write(mainPath,main)disk.write(workersPath,copy(workers))end local function nextId(data,prefix)data.sequence=data.sequence+1 return string.format(\"%s-%s-%d\",prefix,tostring(now()),data.sequence)end local function findQueue(data,jobId)for index,job in ipairs(data.queue)do if job.id==jobId then return index,job end end end local function finish(data,job,status,result)data.running[job.id]=nil local worker=job.worker and data.workers[tostring(job.worker)]if worker then worker.inFlight=math.max(0,(tonumber(worker.inFlight)or 0)-1)end job.status,job.finished,job.result=status,now(),result or{}job.result.output=cleanOutput(job.result.output or job.result.stdout or\"\")data.history[#data.history+1]=job local limit=(context and tonumber(context.historyLimit))or HISTORY_LIMIT while#data.history>limit do table.remove(data.history,1)end end local function addJob(data,templateName,scheduleId)local template=data.templates[templateName]assert(template and template.enabled~=false,\"template unavailable\")local job={id=nextId(data,\"job\"),template=templateName,revision=template.revision,command=template.command,scheduleId=scheduleId,created=now(),status=\"queued\",attempts=0,retrySafe=template.retrySafe==true,maxAttempts=template.maxAttempts,}data.queue[#data.queue+1]=job return job end function M.state()return copy(load())end function M.setController(value)assert(value~=nil and tostring(value)~=\"\",\"controller id required\")local data=load();data.controller=tostring(value);save(data)log(\"jobs_controller\",data.controller)return data.controller end function M.controller()return load().controller end function M.isController()return M.controller()==localId()end local function requireController()assert(M.isController(),\"controller required\")end function M.setCapacity(value)local data=load();data.capacity=clampNumber(value,1,32,DEFAULT_CAPACITY);save(data)return data.capacity end function M.heartbeat(peer,status)assert(peer~=nil and tostring(peer)~=\"\",\"worker id required\")status=type(status)==\"table\"and status or{}local data,id=load(),tostring(peer)local old=data.workers[id]or{}data.workers[id]={id=id,label=status.label or old.label or id,capacity=clampNumber(status.capacity,1,32,old.capacity or DEFAULT_CAPACITY),inFlight=clampNumber(status.inFlight,0,32,old.inFlight or 0),queueDepth=clampNumber(status.queueDepth,0,1024,0),route=status.route or old.route,hops=clampNumber(status.hops or status.routeHops,0,32,old.hops or 99),available=status.available~=false,lastSeen=now(),lastError=status.lastError,}save(data)return copy(data.workers[id])end M.updateWorker=M.heartbeat function M.workers()local data,at,result=load(),now(),{}for id,worker in pairs(data.workers)do local row=copy(worker)row.healthy=row.available~=false and at-(row.lastSeen or 0)<=HEARTBEAT_TIMEOUT row.slots=math.max(0,(row.capacity or DEFAULT_CAPACITY)-(row.inFlight or 0))result[id]=row end return result end function M.addTemplate(name,command,options)requireController()if type(name)==\"table\"then options,command,name=name,name.command,name.id or name.name end assert(type(name)==\"string\"and name:match(\"^[%w_%-]+$\"),\"invalid template name\")assert(type(command)==\"string\"and#command>0 and#command<=512,\"invalid fixed command\")options=type(options)==\"table\"and options or{}local data=load()assert(not data.templates[name],\"template already exists\")data.templates[name]={name=name,command=command,enabled=options.enabled~=false,timeout=clampNumber(options.timeout,1000,600000,DEFAULT_TIMEOUT),retrySafe=options.retrySafe==true,maxAttempts=clampNumber(options.maxAttempts,1,5,DEFAULT_ATTEMPTS),revision=nextId(data,\"template\"),created=now(),}save(data);log(\"jobs_template_added\",name)return copy(data.templates[name])end function M.listTemplates()return copy(load().templates)end function M.setTemplateRetrySafe(name,enabled)requireController()local data=load();assert(data.templates[name],\"unknown template\")data.templates[name].retrySafe=enabled==true;save(data)return copy(data.templates[name])end function M.setTemplateEnabled(name,enabled)requireController()local data=load();assert(data.templates[name],\"unknown template\")data.templates[name].enabled=enabled~=false;save(data)return copy(data.templates[name])end function M.removeTemplate(name)requireController()local data=load();assert(data.templates[name],\"unknown template\")for _,job in ipairs(data.queue)do assert(job.template~=name,\"template has queued jobs\")end for _,job in pairs(data.running)do assert(job.template~=name,\"template has running jobs\")end for _,schedule in pairs(data.schedules)do assert(schedule.template~=name or not schedule.enabled,\"template has active schedule\")end data.templates[name]=nil;save(data);log(\"jobs_template_removed\",name)return true end function M.enqueue(templateName,scheduleId)requireController()local data,job=load(),nil job=addJob(data,templateName,scheduleId)save(data)return copy(job)end function M.selectWorker()local data,at,candidates=load(),now(),{}for id,worker in pairs(data.workers)do local healthy=worker.available~=false and at-(worker.lastSeen or 0)<=HEARTBEAT_TIMEOUT local capacity,inFlight=worker.capacity or DEFAULT_CAPACITY,worker.inFlight or 0 if healthy and inFlight<capacity then candidates[#candidates+1]={id=id,worker=worker,load=inFlight/capacity,hops=tonumber(worker.hops)or tonumber(worker.routeHops)or 99}end end table.sort(candidates,function(a,b)if a.load~=b.load then return a.load<b.load end if a.hops~=b.hops then return a.hops<b.hops end return a.id<b.id end)if#candidates==0 then return nil end local best,count=candidates[1],1 while candidates[count+1]and candidates[count+1].load==best.load and candidates[count+1].hops==best.hops do count=count+1 end local chosen=candidates[(data.fair%count)+1]return copy(chosen.worker)end function M.dispatchNext()local job,worker=M.nextDispatch(),M.selectWorker()if not job or not worker then return nil,job and\"no healthy worker\"or\"no queued job\"end local assigned=M.assign(job,worker.id)if context and type(context.execute)==\"function\"then local template=M.listTemplates()[assigned.template]local ok,first,second=pcall(context.execute,copy(worker),copy(template),copy(assigned))if not ok then return M.fail(assigned.id,first)end if first==false then return M.fail(assigned.id,second or\"worker rejected job\")end if type(first)==\"table\"then return M.complete(assigned.id,first)end end return assigned end function M.run(templateName,scheduleId)assert(scheduleId==nil or type(scheduleId)==\"string\",\"runtime job arguments are not allowed\")local job=M.enqueue(templateName,scheduleId)local dispatched,reason=M.dispatchNext()return dispatched or job,reason end function M.nextDispatch()local data=load()for _,job in ipairs(data.queue)do if job.status==\"queued\"then return copy(job)end end return nil end function M.assign(jobValue,workerValue)local data,jobId,workerId=load(),type(jobValue)==\"table\"and jobValue.id or jobValue,tostring(workerValue)local index,job=findQueue(data,jobId)assert(job and job.status==\"queued\",\"job is not queued\")local worker=data.workers[workerId]assert(worker,\"unknown worker\")assert(worker.available~=false and now()-(worker.lastSeen or 0)<=HEARTBEAT_TIMEOUT,\"worker unavailable\")assert((worker.inFlight or 0)<(worker.capacity or DEFAULT_CAPACITY),\"worker saturated\")table.remove(data.queue,index)job.status,job.worker,job.started=\"running\",workerId,now()job.attempts=(job.attempts or 0)+1 job.deadline=job.started+(data.templates[job.template].timeout or DEFAULT_TIMEOUT)data.running[job.id]=job;worker.inFlight=(worker.inFlight or 0)+1 data.fair=data.fair+1 save(data)return copy(job)end function M.complete(jobId,result,stdout,stderr)local data,job=load(),nil job=data.running[jobId];assert(job,\"unknown running job\")if result==false then return M.fail(jobId,stderr or stdout or\"worker failed\")end local payload if type(result)==\"table\"then payload=copy(result)elseif stdout~=nil or stderr~=nil then payload={ok=result~=false,stdout=tostring(stdout or\"\"),stderr=tostring(stderr or\"\"),output=tostring(stdout or\"\")}else payload={output=tostring(result or\"\")}end finish(data,job,\"complete\",payload)save(data)return copy(job)end function M.fail(jobId,reason)local data,job=load(),nil job=data.running[jobId];assert(job,\"unknown running job\")data.running[job.id]=nil local worker=job.worker and data.workers[tostring(job.worker)]if worker then worker.inFlight=math.max(0,(worker.inFlight or 0)-1)end if job.retrySafe and(job.attempts or 0)<(job.maxAttempts or DEFAULT_ATTEMPTS)then job.status,job.worker,job.deadline=\"queued\",nil,nil job.lastError,job.updated=tostring(reason or\"failed\"),now()data.queue[#data.queue+1]=job save(data)return copy(job),true end finish(data,job,\"failed\",{error=tostring(reason or\"failed\")})save(data)return copy(job),false end function M.schedule(templateName,interval)requireController()interval=tonumber(interval)assert(interval and interval>=1 and interval<=86400,\"interval must be 1..86400 seconds\")local data=load()assert(data.templates[templateName],\"unknown template\")local id=nextId(data,\"schedule\")data.schedules[id]={id=id,template=templateName,interval=interval*1000,enabled=true,nextRun=now()+interval*1000,created=now()}save(data);log(\"jobs_schedule_added\",id)return copy(data.schedules[id])end function M.cancel(scheduleId)requireController()local data=load();assert(data.schedules[scheduleId],\"unknown schedule\")data.schedules[scheduleId].enabled=false;data.schedules[scheduleId].cancelled=now();save(data)return true end function M.history()return copy(load().history)end function M.queue()return copy(load().queue)end function M.running()return copy(load().running)end function M.localStatus()local data=load()return{id=localId(),controller=data.controller,capacity=data.capacity,inFlight=(function()local total=0;for _ in pairs(data.running)do total=total+1 end;return total end)(),queueDepth=#data.queue}end function M.syncTemplates(controllerId,templates)assert(controllerId~=nil and tostring(controllerId)~=\"\",\"controller id required\")assert(type(templates)==\"table\",\"template snapshot required\")local data=load();data.controller,data.templates=tostring(controllerId),{}for name,template in pairs(templates)do assert(type(name)==\"string\"and type(template)==\"table\"and type(template.command)==\"string\",\"invalid template snapshot\")data.templates[name]={name=name,command=template.command,enabled=template.enabled~=false,timeout=clampNumber(template.timeout,1000,600000,DEFAULT_TIMEOUT),retrySafe=template.retrySafe==true,maxAttempts=clampNumber(template.maxAttempts,1,5,DEFAULT_ATTEMPTS),revision=template.revision or\"synced\",created=template.created or now(),}end save(data)return copy(data.templates)end M.scheduleInterval=M.schedule function M.tick()local data,at,added,timedOut,stale=load(),now(),0,0,0 for _,schedule in pairs(data.schedules)do if M.isController()and schedule.enabled and at>=(schedule.nextRun or at)then addJob(data,schedule.template,schedule.id);added=added+1 repeat schedule.nextRun=(schedule.nextRun or at)+schedule.interval until schedule.nextRun>at end end for _,worker in pairs(data.workers)do if worker.available~=false and at-(worker.lastSeen or 0)>HEARTBEAT_TIMEOUT then worker.stale=true;stale=stale+1 end end local expired={}for id,job in pairs(data.running)do if at>=(job.deadline or at)then expired[#expired+1]=id end end for _,id in ipairs(expired)do local job=data.running[id]if job then timedOut=timedOut+1;data.running[id]=nil local worker=job.worker and data.workers[tostring(job.worker)]if worker then worker.inFlight=math.max(0,(worker.inFlight or 0)-1)end if job.retrySafe and(job.attempts or 0)<(job.maxAttempts or DEFAULT_ATTEMPTS)then job.status,job.worker,job.deadline,job.lastError=\"queued\",nil,nil,\"timeout\";data.queue[#data.queue+1]=job else finish(data,job,\"failed\",{error=\"timeout\"})end end end if added>0 or timedOut>0 or stale>0 then save(data)end if context and type(context.execute)==\"function\"then M.dispatchNext()end return{enqueued=added,timedOut=timedOut,staleWorkers=stale}end function M.new(options)options=options or{}local service={}local names={\"state\",\"setController\",\"controller\",\"isController\",\"setCapacity\",\"heartbeat\",\"updateWorker\",\"workers\",\"addTemplate\",\"listTemplates\",\"setTemplateRetrySafe\",\"setTemplateEnabled\",\"removeTemplate\",\"syncTemplates\",\"enqueue\",\"run\",\"nextDispatch\",\"selectWorker\",\"dispatchNext\",\"assign\",\"complete\",\"fail\",\"tick\",\"schedule\",\"scheduleInterval\",\"cancel\",\"history\",\"queue\",\"running\",\"localStatus\",}local function invoke(fn,...)local prior=context;context=options local result={pcall(fn,...)}context=prior if not result[1]then error(result[2],0)end return table.unpack(result,2)end for _,name in ipairs(names)do service[name]=function(...)local args={...}if args[1]==service then table.remove(args,1)end return invoke(M[name],table.unpack(args))end end return service end return M",
  ["ceetos/lib/peripherals.lua"] = "local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local M={}local PATH=\"/ceetos/data/shares.lua\"local SAFE={getItemDetail=true,list=true,size=true,getMetadata=true,getType=true,getMethods=true,getInput=true,getOutput=true,getAngles=true,getVelocity=true,getHeight=true,getAirPressure=true,getSpeed=true,getStress=true,getStressCapacity=true,stock=true,getStockItemDetail=true,getConfiguration=true,getAddress=true,}local function requestMatchesGrid(request,slots)if type(request)~=\"table\"then return false,\"redstone requester did not return its encoded request\"end local exact=true for slot=1,9 do local expected=slots[slot]if expected and expected~=\"minecraft:air\"then local actual=request[slot]or request[tostring(slot)]if type(actual)~=\"table\"or actual.name~=expected then exact=false;break end end end if exact then return true end local expected,actual={},{}for slot=1,9 do if slots[slot]and slots[slot]~=\"minecraft:air\"then expected[#expected+1]=slots[slot]end end for key,value in pairs(request)do local index=tonumber(key)if index and type(value)==\"table\"and type(value.name)==\"string\"and value.name~=\"minecraft:air\"then actual[#actual+1]={index=index,name=value.name}end end table.sort(actual,function(a,b)return a.index<b.index end)if#actual~=#expected then return false,\"redstone requester retained an incomplete crafting request\"end for index,item in ipairs(actual)do if item.name~=expected[index]then return false,\"redstone requester retained different crafting ingredients\"end end return true end local function shares()return store.read(PATH,{})end local function save(s)store.write(PATH,s)end local function safeMethods(name)local out={}local available=peripheral.getMethods and peripheral.getMethods(name)or nil if available then for _,method in ipairs(available)do if SAFE[method]then out[#out+1]=method end end else for method in pairs(SAFE)do out[#out+1]=method end end table.sort(out)return out end function M.listLocal()local result={}for _,name in ipairs(peripheral.getNames())do result[#result+1]={name=name,type=peripheral.getType(name)}end return result end function M.share(name,mode)assert(peripheral.isPresent(name),\"peripheral is not present\")assert(mode==\"peer\"or mode==\"master\",\"mode must be peer or master\")local s=shares()s[name]={mode=mode,methods=SAFE}save(s)audit.log(\"local\",\"peripheral.share\",{name=name,mode=mode})end function M.unshare(name)local s=shares();s[name]=nil;save(s)audit.log(\"local\",\"peripheral.unshare\",{name=name})end function M.shareAll(mode)assert(mode==\"peer\"or mode==\"master\",\"mode must be peer or master\")local count=0 for _,item in ipairs(M.listLocal())do M.share(item.name,mode);count=count+1 end audit.log(\"local\",\"peripheral.share_all\",{mode=mode,count=count})return count end function M.unshareAll()local s,count=shares(),0 for name in pairs(s)do s[name],count=nil,count+1 end save(s)audit.log(\"local\",\"peripheral.unshare_all\",{count=count})return count end function M.describe()local s,result=shares(),{}for name,spec in pairs(s)do if peripheral.isPresent(name)then result[#result+1]={name=name,type=peripheral.getType(name),mode=spec.mode,methods=safeMethods(name)}end end return result end function M.call(peerIsMaster,name,method,args)local spec=shares()[name]if not spec or not peripheral.isPresent(name)then return false,\"not shared\"end if spec.mode==\"master\"and not peerIsMaster then return false,\"master-only peripheral\"end if not spec.methods[method]then return false,\"method not allowed\"end local api=peripheral.wrap(name)if type(api[method])~=\"function\"then return false,\"method unavailable\"end local ok,result=pcall(api[method],table.unpack(args or{}))return ok,result end function M.craftRequest(peerIsMaster,name,address,filters)local spec=shares()[name]local localAccess=peerIsMaster==\"local\"if not peripheral.isPresent(name)then return false,\"stock ticker is unavailable\"end if not localAccess and not spec then return false,\"stock ticker is not shared\"end if not localAccess and spec.mode==\"master\"and not peerIsMaster then return false,\"master-only peripheral\"end if type(address)~=\"string\"or#address<1 or#address>64 or address:find(\"[%z\\1-\\31\\127]\")then return false,\"invalid destination address\"end if type(filters)~=\"table\"or#filters<1 or#filters>32 then return false,\"invalid craft filters\"end local clean={}for index,filter in ipairs(filters)do if type(filter)~=\"table\"or type(filter.name)~=\"string\"or#filter.name>128 then return false,\"invalid item filter \"..tostring(index)end local count=tonumber(filter._requestCount or filter.count)if not count or count<1 or count>256 or count~=math.floor(count)then return false,\"invalid request count\"end clean[index]={name=filter.name,_requestCount=count}end local api=peripheral.wrap(name)if not api or type(api.requestFiltered)~=\"function\"then return false,\"stock ticker requestFiltered is unavailable\"end local ok,result=pcall(api.requestFiltered,address,table.unpack(clean))if ok then audit.log(\"network\",\"peripheral.craft_request\",{name=name,address=address,filters=#clean})end return ok,result end function M.craftingRecipeRequest(peerIsMaster,name,address,batches,grid)local spec=shares()[name]local localAccess=peerIsMaster==\"local\"if not peripheral.isPresent(name)then return false,\"redstone requester is unavailable\"end if not localAccess and not spec then return false,\"redstone requester is not shared\"end if not localAccess and spec.mode==\"master\"and not peerIsMaster then return false,\"master-only peripheral\"end if type(address)~=\"string\"or#address<1 or#address>64 or address:find(\"[%z\\1-\\31\\127]\")then return false,\"invalid craft address\"end batches=tonumber(batches)if not batches or batches<1 or batches>256 or batches~=math.floor(batches)then return false,\"invalid craft batches\"end if type(grid)~=\"table\"then return false,\"invalid crafting grid\"end local slots={}for slot=1,9 do local value=grid[slot]if value~=nil and value~=false and(type(value)~=\"string\"or#value>128)then return false,\"invalid craft grid slot\"end slots[slot]=(value==nil or value==false)and\"minecraft:air\"or value end local api=peripheral.wrap(name)if not api or type(api.setCraftingRequest)~=\"function\"or type(api.setAddress)~=\"function\"or type(api.request)~=\"function\"then return false,\"redstone requester crafting API is unavailable\"end local ok,result=pcall(function()if type(api.setConfiguration)==\"function\"then api.setConfiguration(\"strict\")end api.setCraftingRequest(batches,slots[1],slots[2],slots[3],slots[4],slots[5],slots[6],slots[7],slots[8],slots[9])api.setAddress(address)local configured=nil if type(api.getRequest)==\"function\"then configured=api.getRequest()local retained,retainError=requestMatchesGrid(configured,slots)if not retained then error(retainError)end end if type(api.getAddress)==\"function\"and api.getAddress()~=address then error(\"redstone requester did not retain the craft address\")end local submitted=api.request()if submitted==false then error(\"redstone requester rejected the crafting request\")end return{submitted=true,strict=type(api.setConfiguration)==\"function\",configured=configured~=nil,address=address}end)if ok then audit.log(\"network\",\"peripheral.crafting_recipe_request\",{name=name,address=address,batches=batches})end return ok,result end return M",
  ["ceetos/lib/telemetry.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local PATH=\"/ceetos/data/config.lua\"local function config()return store.read(PATH,{telemetry=false})end local function save(c)store.write(PATH,c)end function M.setEnabled(value)local c=config();c.telemetry=value;save(c)end function M.status()return config().telemetry==true end function M.configuration()local c=config()return{enabled=c.telemetry==true,dashboardUrl=c.dashboard_url,pollUrl=c.dashboard_poll_url,terminal=c.dashboard_terminal==true}end function M.configure(url,pollUrl,token,terminal)local c=config()c.dashboard_url,c.dashboard_poll_url,c.dashboard_token=url,pollUrl,token c.dashboard_terminal=terminal==true save(c)end function M.snapshot(activity)local data={ts=os.epoch(\"utc\"),id=os.getComputerID(),label=os.getComputerLabel(),activity=activity,fuel=turtle and turtle.getFuelLevel()or nil}if gps then local x,y,z=gps.locate(0.2)if x then data.gps={x=x,y=y,z=z}end end return data end function M.post(activity)local c=config()if not c.telemetry or not http or not c.dashboard_url or not c.dashboard_token then return false end local body=textutils.serialiseJSON(M.snapshot(activity))local ok,response=pcall(http.post,c.dashboard_url,body,{[\"Content-Type\"]=\"application/json\",[\"Authorization\"]=\"Bearer \"..c.dashboard_token})if ok and response then response.close();return true end return false end function M.postResult(jobId,payload)local c=config()if not http or not c.dashboard_url or not c.dashboard_token or type(jobId)~=\"string\"then return false end local body=payload or{}body.id=jobId local ok,response=pcall(http.post,c.dashboard_url:gsub(\"/telemetry$\",\"/result\"),textutils.serialiseJSON(body),{[\"Content-Type\"]=\"application/json\",[\"Authorization\"]=\"Bearer \"..c.dashboard_token})if ok and response then response.close();return true end return false end function M.poll(run)local c=config()if not http or not c.dashboard_poll_url or not c.dashboard_token then return end local url=c.dashboard_poll_url..\"?id=\"..textutils.urlEncode(tostring(os.getComputerID()))local ok,response=pcall(http.get,url,{[\"Authorization\"]=\"Bearer \"..c.dashboard_token})if not ok or not response then return end local decoded=textutils.unserialiseJSON(response.readAll());response.close()for _,job in ipairs((decoded and decoded.jobs)or{})do run(job)end end return M",
  ["ceetos/lib/cloud.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local profile=require(\"ceetos.lib.profile\")local M={}M.PATH=\"/ceetos/data/cloud.lua\"M.MAX_BODY=48*1024 M.MAX_RESPONSE=64*1024 M.DEFAULTS={auth=\"https://auth.ceet.uk\",recipe=\"https://recipe.ceet.uk\",dashboard=\"https://dash.ceet.uk\",}local function now()return os.epoch(\"utc\")end local function validUrl(value)if type(value)~=\"string\"or#value>160 then return false end return value:match(\"^https://[%w%-%.]+%.ceet%.uk$\")~=nil end local function validId(value)return type(value)==\"string\"and value:match(\"^[%w_.%-]+$\")~=nil and#value<=96 end local function copy(value)local out={}for key,item in pairs(value or{})do out[key]=item end return out end local function normalise(value)value=type(value)==\"table\"and value or{}local urls=type(value.urls)==\"table\"and value.urls or{}local result={schema=1,enabled=value.enabled==true,urls={auth=validUrl(urls.auth)and urls.auth or M.DEFAULTS.auth,recipe=validUrl(urls.recipe)and urls.recipe or M.DEFAULTS.recipe,dashboard=validUrl(urls.dashboard)and urls.dashboard or M.DEFAULTS.dashboard,},node=type(value.node)==\"table\"and copy(value.node)or nil,}if result.node and(not validId(result.node.id)or type(result.node.secret)~=\"string\"or#result.node.secret<32 or#result.node.secret>192)then result.node=nil end return result end function M.load()store.invalidate(M.PATH)return normalise(store.read(M.PATH,{}))end function M.save(value)local clean=normalise(value)return store.write(M.PATH,clean)end function M.configure(urls)local value=M.load()urls=type(urls)==\"table\"and urls or{}for key in pairs(M.DEFAULTS)do if urls[key]~=nil then if not validUrl(urls[key])then return false,\"invalid \"..key..\" cloud URL\"end value.urls[key]=urls[key]end end return M.save(value)end function M.clear()local value=M.load();value.enabled,value.node=false,nil return M.save(value)end function M.publicStatus()local value=M.load()return{enabled=value.enabled,enrolled=value.node~=nil,node=value.node and value.node.id or nil,urls=copy(value.urls),profile=profile.id()}end local function readBounded(handle,limit)local chunks,total={},0 while true do local part=handle.read(math.min(4096,limit-total+1))if not part or part==\"\"then break end total=total+#part if total>limit then return nil,\"cloud response is too large\"end chunks[#chunks+1]=part end return table.concat(chunks)end local function json(value)local ok,result=pcall(textutils.unserialiseJSON,value)return ok and type(result)==\"table\"and result or nil end local function canonical(node,timestamp,nonce,method,path,body)local digest=assert(password.sha256(body))return table.concat({\"ceetos-cloud-node-v1\",node,tostring(timestamp),nonce,method,path,digest},\"\\n\")end local nonceCounter=0 local function nonce()nonceCounter=nonceCounter+1 return password.newSalt(\"cloud:\"..tostring(nonceCounter))end local function perform(url,method,path,body,headers)if not http or type(http.post)~=\"function\"then return nil,\"HTTP is unavailable\"end local response local ok,err=pcall(function()response=http.post(url..path,body,headers)end)if not ok or not response then return nil,tostring(err or\"cloud request failed\")end local raw,readErr=readBounded(response,M.MAX_RESPONSE)pcall(response.close)if not raw then return nil,readErr end local decoded=json(raw)if not decoded then return nil,\"cloud returned invalid JSON\"end if decoded.ok==false then return nil,tostring(decoded.error or\"cloud rejected request\"):sub(1,180)end return decoded end function M.enroll(code)if type(code)~=\"string\"or#code<12 or#code>128 or code:find(\"[%z\\1-\\31\\127]\")then return nil,\"invalid enrollment code\"end local value=M.load()local payload=textutils.serialiseJSON({code=code,profile=profile.id(),label=os.getComputerLabel()or(\"Computer \"..tostring(os.getComputerID())),computerId=tostring(os.getComputerID())})if#payload>M.MAX_BODY then return nil,\"enrollment request is too large\"end local result,err=perform(value.urls.auth,\"POST\",\"/v1/nodes/enroll\",payload,{[\"Content-Type\"]=\"application/json\"})if not result then return nil,err end if type(result.node)~=\"table\"or not validId(result.node.id)or type(result.node.secret)~=\"string\"or#result.node.secret<32 then return nil,\"cloud returned invalid enrollment\"end value.enabled,value.node=true,{id=result.node.id,secret=result.node.secret,enrolledAt=now()}local saved,saveErr=M.save(value)if not saved then return nil,saveErr or\"could not save cloud enrollment\"end return M.publicStatus()end function M.call(service,path,payload)local value=M.load()if not value.enabled or not value.node then return nil,\"cloud node is not enrolled\"end if not M.DEFAULTS[service]or type(path)~=\"string\"or not path:match(\"^/v1/[%w%-%._/]+$\")or#path>128 then return nil,\"invalid cloud endpoint\"end local body=textutils.serialiseJSON(type(payload)==\"table\"and payload or{})if#body>M.MAX_BODY then return nil,\"cloud request is too large\"end local timestamp,requestNonce=now(),nonce()local signature=password.hmacSha256(value.node.secret,canonical(value.node.id,timestamp,requestNonce,\"POST\",path,body))if not signature then return nil,\"could not sign cloud request\"end return perform(value.urls[service],\"POST\",path,body,{[\"Content-Type\"]=\"application/json\",[\"X-CeetOS-Node\"]=value.node.id,[\"X-CeetOS-Time\"]=tostring(timestamp),[\"X-CeetOS-Nonce\"]=requestNonce,[\"X-CeetOS-Signature\"]=signature,})end return M",
  ["ceetos/lib/cloud_sync.lua"] = "local cloud=require(\"ceetos.lib.cloud\")local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local profile=require(\"ceetos.lib.profile\")local TELEMETRY_MODULE=\"ceetos.lib.\"..\"telemetry\"local RECIPE_SERVICE_MODULE=\"ceetos.lib.\"..\"recipe_service\"local M={}local STATE=\"/ceetos/data/cloud-state.lua\"local INTERVALS={telemetry=60000,authority=30000,recipes=30000}local function now()return os.epoch(\"utc\")end local function defaults()return{schema=1,revision=0,auth={online=false,next=0},recipe={online=false,next=0,revision=0},telemetry={next=0},lastError=nil}end local function state()store.invalidate(STATE)local value=store.read(STATE,defaults())for key,fallback in pairs(defaults())do if value[key]==nil then value[key]=fallback end end for key,fallback in pairs(defaults().recipe)do if value.recipe[key]==nil then value.recipe[key]=fallback end end return value end local function save(value)value.revision=(tonumber(value.revision)or 0)+1 return store.write(STATE,value)end local function errorState(value,message)value.lastError=tostring(message or\"cloud request failed\"):sub(1,180)end function M.status()local value,cfg=state(),cloud.publicStatus()return{enabled=cfg.enabled,enrolled=cfg.enrolled,node=cfg.node,urls=cfg.urls,authOnline=value.auth.online==true,recipeOnline=value.recipe.online==true,lastSync=value.lastSync,lastError=value.lastError}end function M.configure(urls)return cloud.configure(urls)end function M.enroll(code)return cloud.enroll(code)end function M.clear()local ok,err=cloud.clear();if ok then store.write(STATE,defaults())end return ok,err end function M.authOnline()return M.status().authOnline==true end function M.recipeOnline()return M.status().recipeOnline==true end local function cloudAuthority(result)local auth=require(\"ceetos.lib.auth\")if type(result)~=\"table\"or type(result.directory)~=\"table\"then return false,\"invalid cloud authority response\"end local cfg=cloud.publicStatus()local record={id=\"cloud:\"..tostring(cfg.urls.auth),term=tonumber(result.term)or 0,revision=tonumber(result.revision)or 0,directory=result.directory}return pcall(auth.acceptAuthority,record)end function M.login(name,plainPassword)if type(name)~=\"string\"or type(plainPassword)~=\"string\"then return false,\"invalid credentials\"end local begin,beginErr=cloud.call(\"auth\",\"/v1/auth/login/begin\",{username=name,node=tostring(os.getComputerID())})if not begin then return false,beginErr end if type(begin.salt)~=\"string\"or type(begin.workFactor)~=\"number\"or type(begin.challenge)~=\"string\"or type(begin.expires)~=\"number\"then return false,\"invalid cloud login challenge\"end local verifier,deriveErr=password.derive(plainPassword,begin.salt,begin.workFactor)if not verifier then return false,deriveErr end local proof=password.hmacSha256(verifier,table.concat({\"ceetos/auth/login/v1\",tostring(begin.authority),tostring(os.getComputerID()),tostring(begin.requestId),tostring(begin.challenge),tostring(begin.expires)},\"|\"))local result,resultErr=cloud.call(\"auth\",\"/v1/auth/login/proof\",{username=name,node=tostring(os.getComputerID()),requestId=begin.requestId,challenge=begin.challenge,proof=proof})if not result then return false,resultErr end local auth=require(\"ceetos.lib.auth\")local saved,saveErr=auth.saveCentralSession(result.session)if not saved then return false,saveErr end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end function M.mutate(action,fields)local auth=require(\"ceetos.lib.auth\")local current=auth.refreshSession()if not current or not current.central then return false,\"cloud login is required\"end local result,err=cloud.call(\"auth\",\"/v1/auth/mutate\",{action=action,fields=fields,session=current.nonce,node=tostring(os.getComputerID()),revision=current.accountRevision})if not result then return false,err end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end function M.recipePlan(item,quantity,stock)if not M.recipeOnline()then return nil,\"cloud Recipe Server is unavailable\"end local result,err=cloud.call(\"recipe\",\"/v1/recipe/plan\",{item=item,quantity=quantity,stock=stock})if not result then return nil,err end return result.plan,result.error end local function telemetry(activity)local snapshot={ts=now(),id=os.getComputerID(),label=os.getComputerLabel(),activity=activity}if profile.is(\"desktop\")then local loaded,telemetryService=pcall(require,TELEMETRY_MODULE)if loaded and telemetryService.status()then snapshot=telemetryService.snapshot(activity)end end local net=require(\"ceetos.lib.net\")local transport=net.statusSnapshot and net.statusSnapshot().transport or{}snapshot.health={profile=profile.id(),version=require(\"ceetos.lib.version\").string,uptime=os.clock(),freeSpace=fs.getFreeSpace(\"/\"),peers=#(net.peers and net.peers()or{}),healthyLinks=transport and transport.healthyLinks or 0,lastError=M.status().lastError,}local result,err=cloud.call(\"dashboard\",\"/v1/telemetry/ingest\",snapshot)return result~=nil,err end local function syncAuth()if profile.is(\"auth-server\")then local auth=require(\"ceetos.lib.auth\")local snapshot=auth.authoritySnapshot()local pushed,pushErr=cloud.call(\"auth\",\"/v1/auth/replica/merge\",{snapshot=snapshot,source=\"mesh\"})if not pushed then return false,pushErr end if type(pushed.snapshot)==\"table\"then auth.merge(pushed.snapshot,true)end local ok,authErr=cloudAuthority(pushed)if not ok then return false,authErr end return true end local result,err=cloud.call(\"auth\",\"/v1/auth/directory\",{})if not result then return false,err end local ok,authErr=cloudAuthority(result)if not ok then return false,authErr end return true end local function syncRecipes()if not profile.is(\"recipe-server\")then local result,err=cloud.call(\"recipe\",\"/v1/recipe/status\",{})return result~=nil,err end local service=require(RECIPE_SERVICE_MODULE)if type(service.exportOverlay)~=\"function\"then return false,\"recipe export unavailable\"end local payload,exportErr=service.exportOverlay()if not payload then return false,exportErr end local value=state()value.recipe=value.recipe or{revision=0}local result,err=cloud.call(\"recipe\",\"/v1/recipe/replica/merge\",{overlay=payload,baseRevision=math.max(0,math.floor(tonumber(value.recipe.revision)or 0)),})if not result then return false,err end if type(result.overlay)==\"table\"and type(service.replaceOverlay)==\"function\"then local applied,applyErr=service.replaceOverlay(result.overlay)if not applied then return false,applyErr end end return true,nil,math.max(0,math.floor(tonumber(result.revision)or 0))end function M.tick(activity)local cfg=cloud.publicStatus();if not cfg.enabled or not cfg.enrolled then return false,\"cloud node is not enrolled\"end local value,timestamp=state(),now()local changed=false if timestamp>=(tonumber(value.auth.next)or 0)then local ok,err=syncAuth();value.auth.online,value.auth.next=ok==true,timestamp+INTERVALS.authority if not ok then errorState(value,err)else value.lastError=nil end changed=true end if timestamp>=(tonumber(value.recipe.next)or 0)then local ok,err,recipeRevision=syncRecipes();value.recipe.online,value.recipe.next=ok==true,timestamp+INTERVALS.recipes if recipeRevision~=nil then value.recipe.revision=recipeRevision end if not ok then errorState(value,err)end changed=true end if timestamp>=(tonumber(value.telemetry.next)or 0)then local ok,err=telemetry(activity or\"desktop\");value.telemetry.next=timestamp+INTERVALS.telemetry if not ok then errorState(value,err)end changed=true end if changed then value.lastSync=timestamp;save(value)end return true end return M",
  ["ceetos/lib/owner_service.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local telemetry=require(\"ceetos.lib.telemetry\")local M={}local PATH=\"/ceetos/data/owner-service.lua\"M.VERSION=1 M.MAX_AGE_MS=60*1000 M.MAX_USED_NONCES=64 local actions={snapshot=true,status=true}local function now()return os.epoch(\"utc\")end local function state()local value=store.read(PATH,{})if type(value)~=\"table\"then value={}end if value.enabled==nil then value.enabled=true end value.version=M.VERSION if type(value.used)~=\"table\"then value.used={}end return value end local function save(value)return store.write(PATH,value)end local function validToken(token)return type(token)==\"string\"and#token>=32 and#token<=128 and not token:find(\"[%z\\1-\\31\\127]\")end local function validId(value)return type(value)==\"string\"and value:match(\"^[%w_%-]+$\")and#value>=8 and#value<=96 end local function validNonce(value)return type(value)==\"string\"and value:match(\"^[%x]+$\")and#value>=16 and#value<=128 end local function canonical(node,request)return table.concat({\"ceetos-owner-service-v1\",tostring(node),tostring(request.id),tostring(request.action),tostring(math.floor(tonumber(request.ts)or-1)),tostring(request.nonce),},\"\\n\")end local function prune(value,time)local kept,rows={},{}for nonce,expires in pairs(value.used or{})do expires=tonumber(expires)if validNonce(nonce)and expires and expires>time then rows[#rows+1]={nonce=nonce,expires=expires}end end table.sort(rows,function(a,b)return a.expires>b.expires end)for index,row in ipairs(rows)do if index<=M.MAX_USED_NONCES then kept[row.nonce]=row.expires end end value.used=kept end function M.status()local value=state()prune(value,now())return{enabled=value.enabled~=false,provisioned=validToken(value.token),owner=type(value.owner)==\"string\"and value.owner or nil,actions={\"snapshot\",\"status\"},}end function M.setEnabled(enabled)local value=state();value.enabled=enabled==true;prune(value,now());save(value)return M.status()end function M.provision(token,owner)assert(validToken(token),\"owner-service token must be 32-128 printable characters\")assert(type(owner)==\"string\"and owner:match(\"^[%w_%-]+$\")and#owner<=32,\"invalid owner\")local value=state()value.token,value.owner,value.enabled,value.used=token,owner,true,{}assert(save(value),\"could not save owner-service configuration\")return M.status()end function M.rotate(owner)local token=password.newSalt(\"owner-service-a\")..password.newSalt(\"owner-service-b\")M.provision(token,owner)return token end function M.signature(token,node,request)assert(validToken(token),\"invalid owner-service token\")return assert(password.hmacSha256(token,canonical(node,request)))end function M.verify(request)if type(request)~=\"table\"then return false,\"service request must be an object\"end local value,time=state(),now()prune(value,time)if value.enabled==false then return false,\"owner service is disabled\"end if not validToken(value.token)then return false,\"owner service has not been provisioned\"end if not validId(request.id)or not actions[request.action]or not validNonce(request.nonce)then return false,\"invalid service request\"end local timestamp=tonumber(request.ts)if not timestamp or math.floor(timestamp)~=timestamp or math.abs(time-timestamp)>M.MAX_AGE_MS then return false,\"service request expired\"end if value.used[request.nonce]then return false,\"service request was already used\"end if type(request.tag)~=\"string\"or not request.tag:match(\"^[%x]+$\")or#request.tag~=64 then return false,\"invalid service authenticator\"end local expected=M.signature(value.token,os.getComputerID(),request)if not password.constantTimeEqual(expected,request.tag:lower())then return false,\"service authentication failed\"end value.used[request.nonce]=timestamp+M.MAX_AGE_MS prune(value,time)assert(save(value),\"could not persist service replay state\")return true,value end function M.dispatch(request)local ok,value=M.verify(request)if not ok then return false,value end if request.action==\"snapshot\"then return true,{action=\"snapshot\",telemetry=telemetry.snapshot(\"owner-service\"),owner=value.owner}end return true,{action=\"status\",service=M.status(),telemetryEnabled=telemetry.status()}end return M",
  ["ceetos/lib/gui.lua"] = "local M={}local store=require(\"ceetos.lib.store\")local terminalUi=require(\"ceetos.lib.terminal_ui\")M.pages={home=\"Desktop\",peers=\"Peers\",peripherals=\"Devices\",telemetry=\"Telemetry\",updates=\"Updates\",admin=\"Administration\",audit=\"Audit log\",jobs=\"Jobs\",files=\"Explorer\"}local function add(items,allowed,item)if allowed then items[#items+1]=item end end local function role(deps,required)return deps.auth.allowed(required)end local function colour(item)if item.kind==\"directory\"then return colors.lightBlue end if item.kind==\"program\"then return colors.lime end if item.kind==\"file\"then return colors.white end return colors.lightGray end local function system(id,label)return{id=id,label=label,kind=\"system\",action=id}end local function count(value)if type(value)~=\"table\"then return 0 end local total=0 for _ in pairs(value)do total=total+1 end return total end local function entries(value)if type(value)~=\"table\"then return{}end local out={}for key,item in pairs(value)do if type(item)==\"table\"then local copy={}for field,fieldValue in pairs(item)do copy[field]=fieldValue end copy._ceetosKey=tostring(key)item=copy else item={value=item,_ceetosKey=tostring(key)}end out[#out+1]=item end table.sort(out,function(left,right)return tostring(left.name or left.id or left._ceetosKey)<tostring(right.name or right.id or right._ceetosKey)end)return out end local function short(value,limit)value=terminalUi.clean(value or\"-\")return#value>limit and value:sub(1,math.max(1,limit-3))..\"...\"or value end function M.newState()return{selected=nil,desktopPage=1,menu=nil,path=\"/\",history={},trash=false,peripheralScroll=1}end function M.model(page,deps,state)state=state or M.newState()local items,lines={},{}local auth,net,peripherals,telemetry,cloud=deps.auth,deps.net,deps.peripherals,deps.telemetry,deps.cloud local account=auth.current and(auth.current.name..\" (\"..auth.current.role..\")\")or\"not logged in\"local liveNetwork=store.read(\"/ceetos/data/network-status.lua\",{})local visiblePeers=liveNetwork.peers or net.peers()local craftState=store.read(\"/ceetos/data/crafting-state.lua\",{})local craftSources=craftState.sources or{}local selectedTicker=craftState.config and craftState.config.ticker local craftingAvailable=selectedTicker~=nil or#(craftSources.tickers or{})>0 if not craftingAvailable and peripheral and peripheral.getNames then for _,name in ipairs(peripheral.getNames())do if tostring(peripheral.getType(name)or\"\"):lower():find(\"stockticker\",1,true)then craftingAvailable=true;break end end end if page==\"home\"then add(items,role(deps,\"operator\"),system(\"terminal\",\"Terminal\"))if role(deps,\"viewer\")then for _,program in ipairs(deps.files.programs())do items[#items+1]={id=\"file:\"..program.path,label=program.name,kind=\"program\",path=program.path,runnable=true,canRun=true,canModify=role(deps,\"operator\"),protected=program.protected,action=\"run:\"..program.path}end end if craftingAvailable then items[#items+1]=system(\"crafting\",\"Crafting\")end if role(deps,\"operator\")then items[#items+1]=system(\"page:files\",\"Files\")items[#items+1]=system(\"page:peers\",\"Peers\")items[#items+1]=system(\"page:peripherals\",\"Devices\")items[#items+1]=system(\"page:telemetry\",\"Telemetry\")items[#items+1]=system(\"page:jobs\",\"Jobs\")items[#items+1]=system(\"page:updates\",\"Updates\")end items[#items+1]=system(\"page:admin\",\"Admin\")return{page=page,title=\"CeetOS Desktop\",desktop=true,items=items,subtitle=account..\"  |  \"..#visiblePeers..\" network peers  |  \"..#peripherals.describe()..\" shared  |  telemetry \"..(telemetry.status()and\"on\"or\"off\"),}elseif page==\"files\"then local source=state.trash and deps.files.trashList()or deps.files.list(state.path)for _,entry in ipairs(source)do local kind=entry.directory and\"directory\"or(entry.runnable and\"program\"or\"file\")items[#items+1]={id=(entry.trashed and\"trash:\"..entry.id or\"file:\"..entry.path),label=entry.name,kind=kind,path=entry.path,original=entry.original,trashId=entry.id,parent=entry.parent,directory=entry.directory,runnable=entry.runnable,protected=entry.protected,trashed=entry.trashed,canRun=role(deps,\"viewer\"),canModify=role(deps,\"operator\"),action=entry.parent and\"up:\"..entry.path or(entry.directory and\"dir:\"..entry.path or(entry.runnable and role(deps,\"viewer\")and\"run:\"..entry.path or nil)),}end return{page=page,title=\"CeetOS Explorer\",items=items,path=state.trash and\"Trash\"or state.path,trash=state.trash}elseif page==\"peers\"then local live=liveNetwork local diag=live.discovery or(net.discoveryStatus and net.discoveryStatus()or{})local session,transport=diag.session,diag.transport or live.transport or{}local peerList,directCount,routedCount,rekeyCount=visiblePeers,0,0,0 for _,peer in ipairs(peerList)do if peer.routed then routedCount=routedCount+1 else directCount=directCount+1 end if peer.rekeyRequired then rekeyCount=rekeyCount+1 end end local discoveryActive=diag.active==true or session~=nil local discoveryState=discoveryActive and tostring((session and session.state)or diag.state or\"inactive\")or\"inactive\"local discoveryText=\"Discovery: \"..discoveryState if session and tonumber(session.expires)then local remaining=math.max(0,math.ceil((tonumber(session.expires)-os.epoch(\"utc\"))/1000))discoveryText=discoveryText..\" (\"..tostring(remaining)..\"s remaining)\"end if session and session.retries then discoveryText=discoveryText..\" (retries \"..tostring(session.retries)..\")\"end if session and session.candidate then discoveryText=discoveryText..\" - candidate \"..tostring(session.candidate.label or session.candidate.id)end local modemText=transport.wireless and(\"wireless links: \"..tostring(transport.wirelessLinkCount or 1))or\"no wireless modem\"local mesh=transport.mesh or{}lines={\"Network peers: \"..(directCount+routedCount)..\" (\"..directCount..\" direct, \"..routedCount..\" routed)\",discoveryText,modemText,\"Protocol: v\"..tostring(live.protocol or transport.protocol or 2)..\" | Rekey required: \"..tostring(rekeyCount),\"Links: \"..tostring(transport.healthyLinks or 0)..\" healthy / \"..tostring(transport.degradedLinks or 0)..\" degraded / \"..tostring(transport.offlineLinks or 0)..\" offline\",\"Mesh: \"..(mesh.enrolled and(\"enrolled e\"..tostring(mesh.epoch))or\"enrollment pending\"),\"Last receive distance: \"..tostring(transport.lastDistance or\"-\")}for _,peer in ipairs(peerList)do local kind=peer.routed and\"Routed\"or\"Direct\"local route=peer.routed and(\" via \"..tostring(peer.next or\"?\")..\" (\"..tostring(peer.hops or\"?\")..\" hop)\")or(peer.auto and\" mesh edge\"or\"\")lines[#lines+1]=kind..\": \"..tostring(peer.label or(\"Computer \"..tostring(peer.id)))..\" (\"..tostring(peer.id)..\")\"..route..\" [\"..tostring(peer.health or\"unknown\")..\"]\"end if not discoveryActive and diag.lastState and diag.lastState~=\"inactive\"then lines[#lines+1]=\"Previous discovery: \"..tostring(diag.lastState)..(diag.lastMessage and(\" - \"..tostring(diag.lastMessage))or\"\")elseif diag.message then lines[#lines+1]=\"Network: \"..tostring(diag.message)end local canBegin=not discoveryActive add(items,role(deps,\"operator\")and canBegin,system(\"start_discovery\",\"Start discovery\"))add(items,role(deps,\"operator\")and canBegin,system(\"join_discovery\",\"Join discovery\"))add(items,role(deps,\"operator\")and discoveryState==\"candidate\",system(\"confirm_discovery\",\"Confirm pairing\"))add(items,role(deps,\"operator\")and not canBegin,system(\"cancel_discovery\",\"Cancel discovery\"))add(items,role(deps,\"operator\"),system(\"remote_refresh\",\"Refresh remote peripherals\"))elseif page==\"peripherals\"then local shared={}for _,item in ipairs(peripherals.describe())do shared[item.name]=item.mode end local detected=peripherals.listLocal()lines={\"Detected devices: \"..#detected,\"Shared devices: \"..#peripherals.describe()}if not state.peripheralView then add(items,role(deps,\"operator\"),system(\"peripheral_view\",\"View peripherals\"))end for _,item in ipairs(detected)do local mode=shared[item.name]items[#items+1]={id=\"peripheral_check:\"..item.name..\":peer\",name=item.name,label=item.name,type=item.type,kind=\"peripheral\",sharedMode=mode,mode=\"peer\",checked=mode==\"peer\",toggle=true,action=\"peripheral_set:\"..item.name..\":peer\"}items[#items+1]={id=\"peripheral_check:\"..item.name..\":master\",name=item.name,label=item.name,type=item.type,kind=\"peripheral\",sharedMode=mode,mode=\"master\",checked=mode==\"master\",toggle=true,action=\"peripheral_set:\"..item.name..\":master\"}end add(items,role(deps,\"operator\"),system(\"share_all:peer\",\"Share all - peers\"))add(items,role(deps,\"operator\"),system(\"share_all:master\",\"Share all - master only\"))add(items,role(deps,\"operator\"),system(\"unshare_all\",\"Unshare all\"))add(items,role(deps,\"operator\"),system(\"remote_cache\",\"View remote cache\"))add(items,role(deps,\"operator\"),system(\"remote_call\",\"Call remote peripheral\"))local shown={}for _,entry in ipairs(items)do local checkbox=entry.id:sub(1,17)==\"peripheral_check:\"local bulk=entry.action==\"share_all:peer\"or entry.action==\"share_all:master\"or entry.action==\"unshare_all\"if state.peripheralView or(not checkbox and not bulk)then shown[#shown+1]=entry end end items=shown elseif page==\"telemetry\"then local c=telemetry.configuration()local cloudStatus=cloud and cloud.status and cloud.status()or{}lines={\"Telemetry: \"..(c.enabled and\"enabled\"or\"disabled\"),\"Cloud node: \"..(cloudStatus.enrolled and(\"enrolled as \"..tostring(cloudStatus.node))or\"not enrolled\"),\"Cloud auth: \"..(cloudStatus.authOnline and\"online\"or\"offline\")..\" | recipes: \"..(cloudStatus.recipeOnline and\"online\"or\"offline\"),\"Dashboard: dash.ceet.uk (Cloudflare Access protected)\",cloudStatus.lastError and(\"Cloud error: \"..short(cloudStatus.lastError,58))or\"Cloud sync is asynchronous and does not block this UI.\",}add(items,role(deps,\"admin\"),system(\"telemetry_toggle\",c.enabled and\"Disable telemetry\"or\"Enable telemetry\"))add(items,role(deps,\"admin\"),system(\"dashboard_configure\",\"Configure dashboard\"))add(items,role(deps,\"admin\")and not cloudStatus.enrolled,system(\"cloud_enroll\",\"Enroll this computer with CeetOS Cloud\"))add(items,role(deps,\"operator\")and cloudStatus.enrolled,system(\"cloud_retry\",\"Retry cloud sync now\"))add(items,role(deps,\"admin\")and cloudStatus.enrolled,system(\"cloud_clear\",\"Remove cloud enrollment\"))elseif page==\"updates\"then local update=store.read(\"/ceetos/data/update-status.lua\",{})local cache,transfer=update.cache,update.transfer lines={\"Installed: \"..tostring(update.version or\"unknown\")..\"  |  \"..tostring(update.status or\"idle\"),update.message and short(update.message,72)or\"Only locally confirmed signed releases can be applied.\",cache and(\"Verified cache: \"..tostring(cache.version)..\"  \"..tostring(cache.fingerprint or\"-\")..\"  key \"..tostring(cache.keyId or\"-\"))or\"Verified cache: none\",transfer and(\"Downloading: \"..tostring(transfer.received or 0)..\"/\"..tostring(transfer.size or\"?\")..\" bytes from \"..tostring(transfer.label or transfer.source))or\"No active download\",}add(items,role(deps,\"operator\"),system(\"update_check\",\"Check for signed releases\"))for _,offer in ipairs(update.offers or{})do local route=offer.transport==\"routed\"and\"routed\"or\"direct\"items[#items+1]={id=\"update-source:\"..tostring(offer.key or offer.id),label=\"Download \"..tostring(offer.version)..\" from \"..short(offer.label or offer.id,18)..\" (\"..route..\")\",kind=\"update\",action=\"update_download:\"..tostring(offer.key or offer.id),offer=offer}end add(items,role(deps,\"operator\")and transfer~=nil,system(\"update_cancel\",\"Cancel download\"))add(items,role(deps,\"operator\")and cache~=nil,system(\"update_apply\",\"Apply verified cached release\"))add(items,role(deps,\"operator\")and cache~=nil and transfer==nil,system(\"update_clear\",\"Clear release cache\"))elseif page==\"admin\"then lines={\"Session: \"..account}add(items,auth.current==nil,system(\"login\",\"Login\"))add(items,auth.current~=nil,system(\"logout\",\"Logout\"))if role(deps,\"operator\")then for _,user in ipairs(auth.list())do items[#items+1]={id=\"account:\"..user.name,label=user.name..\"  (\"..user.role..\")\",kind=\"account\",name=user.name,userRole=user.role,canGrantAdmin=role(deps,\"admin\"),active=auth.current and auth.current.name==user.name}end add(items,true,system(\"user_add\",\"Create user\"))add(items,true,system(\"recovery_enable\",\"Enable recovery window\"))add(items,true,system(\"recovery_reset\",\"Recovery reset\"))add(items,true,system(\"page:audit\",\"View audit log\"))add(items,true,system(\"page:jobs\",\"Jobs\"))end elseif page==\"audit\"then lines={\"Latest audit events:\"}for _,event in ipairs(deps.audit.recent(10))do lines[#lines+1]=event.actor..\" - \"..event.action end elseif page==\"jobs\"then store.invalidate(\"/ceetos/data/jobs.lua\")store.invalidate(\"/ceetos/data/job-workers.lua\")local jobState=store.read(\"/ceetos/data/jobs.lua\",{})local workerState=store.read(\"/ceetos/data/job-workers.lua\",{})local controller=jobState.controllerId or jobState.masterId or workerState.controllerId or workerState.masterId local workers=workerState.workers or jobState.workers or{}local queue=jobState.queue or{}local schedules=jobState.schedules or{}local history=jobState.history or jobState.results or{}lines={\"Controller: \"..tostring(controller or\"electing / unavailable\"),\"Workers: \"..count(workers)..\"  |  Queue: \"..count(queue),\"Schedules: \"..count(schedules)..\"  |  Recent results: \"..count(history),role(deps,\"admin\")and\"Administrator view: scheduler controls are available through nx jobs.\"or\"Operator view: scheduler state is read-only.\",}for _,worker in ipairs(entries(workers))do local name=worker.label or worker.name or worker.id or worker._ceetosKey local active=worker.inFlight or worker.active or 0 local capacity=worker.capacity or 1 local health=worker.available==false and\"offline\"or(worker.health or\"ready\")items[#items+1]={id=\"job-worker:\"..tostring(worker.id or worker._ceetosKey),label=\"Worker  \"..short(name,18)..\"  \"..tostring(active)..\"/\"..tostring(capacity)..\"  \"..short(health,10),kind=\"job\"}end for _,schedule in ipairs(entries(schedules))do local name=schedule.name or schedule.template or schedule.id or schedule._ceetosKey local interval=schedule.interval or schedule.every or\"once\"local status=schedule.enabled==false and\"paused\"or(schedule.status or\"scheduled\")items[#items+1]={id=\"job-schedule:\"..tostring(schedule.id or schedule._ceetosKey),label=\"Schedule  \"..short(name,17)..\"  \"..short(interval,10)..\"  \"..short(status,10),kind=\"job\"}end for _,result in ipairs(entries(history))do local name=result.template or result.name or result.jobId or result.id or result._ceetosKey local status=result.status or(result.ok==false and\"failed\"or\"complete\")local worker=result.worker or result.workerId or\"-\"items[#items+1]={id=\"job-result:\"..tostring(result.id or result._ceetosKey),label=\"Result  \"..short(name,15)..\"  \"..short(status,10)..\"  \"..short(worker,10),kind=\"job\"}end end return{page=page,title=\"CeetOS | \"..(M.pages[page]or page),items=items,lines=lines,remoteResult=page==\"peripherals\"and state.remoteResult or nil}end local function indexOf(model,id)for index,item in ipairs(model.items)do if item.id==id then return index end end end function M.selected(model,state)if#model.items==0 then return nil end local index=indexOf(model,state.selected)if not index then index=1;state.selected=model.items[1].id end return model.items[index],index end function M.select(model,state,item)if item then state.selected=item.id end return item end function M.move(model,state,direction)local _,index=M.selected(model,state)if not index then return end local delta=direction==\"left\"and-1 or direction==\"right\"and 1 or direction==\"up\"and-(model.columns or 1)or(model.columns or 1)local nextIndex=math.max(1,math.min(#model.items,index+delta))state.selected=model.items[nextIndex].id end function M.context(item)if not item or item.parent then return{}end local copy=item.label and{system(\"copy_label:\"..(item.id or\"\"),\"Copy text\")}or{}if item.kind==\"account\"then local actions={system(\"user_role:\"..item.name..\":viewer\",\"Set role: viewer\"),system(\"user_role:\"..item.name..\":operator\",\"Set role: operator\")}if item.canGrantAdmin then actions[#actions+1]=system(\"user_role:\"..item.name..\":admin\",\"Set role: admin\")end actions[#actions+1]=system(\"user_reset:\"..item.name,\"Reset password\")actions[#actions+1]=system(\"user_delete:\"..item.name,\"Delete user\")for _,action in ipairs(copy)do actions[#actions+1]=action end return actions end if item.kind==\"peripheral\"then local actions={}if item.sharedMode then actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":peer\",\"Share with peers\")actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":master\",\"Share with master only\")actions[#actions+1]=system(\"peripheral_unshare:\"..item.name,\"Stop sharing\")else actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":peer\",\"Share with peers\")actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":master\",\"Share with master only\")end return actions end if item.trashed then copy[#copy+1]=system(\"restore:\"..item.trashId,\"Restore\");copy[#copy+1]=system(\"delete_trash:\"..item.trashId,\"Delete permanently\");return copy end if not item.path then if item.action then copy[#copy+1]=system(item.action,\"Open\")end;return copy end local actions={}if item.action and(not item.runnable or item.canRun~=false)then actions[#actions+1]=system(item.action,item.directory and\"Open\"or\"Run\")end if not item.protected and item.canModify then if not item.directory then actions[#actions+1]=system(\"edit:\"..item.path,\"Edit\")end actions[#actions+1]=system(\"rename:\"..item.path,\"Rename\")actions[#actions+1]=system(\"trash:\"..item.path,\"Move to Trash\")end for _,action in ipairs(copy)do actions[#actions+1]=action end return actions end local function drawHeader(model,banner,width,height)term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()terminalUi.clearLine(1,colors.blue);terminalUi.write(2,1,model.title,colors.white,colors.blue,width-2)if height>=2 then terminalUi.write(2,2,model.subtitle or model.path or\"\",colors.lightGray,colors.black,width-2)end if banner and height>=3 then terminalUi.write(2,3,banner.text,banner.error and colors.red or colors.lightGray,colors.black,width-2)end return banner and 4 or 3 end local function drawRemoteResult(result,top,width,height,targets)if not result or top>=height then return top end local pending=result.pending==true or result.status==\"pending\"local failed=result.error==true or result.ok==false or result.status==\"error\"local label=pending and\"Remote request pending\"or(failed and\"Remote request failed\"or\"Remote result\")if result.peer then label=label..\" | \"..tostring(result.peer)end if result.device then label=label..\" | \"..tostring(result.device)end if result.method then label=label..\".\"..tostring(result.method)end terminalUi.clearLine(top,failed and colors.black or colors.gray)terminalUi.write(2,top,label,failed and colors.red or colors.white,failed and colors.black or colors.gray,width-4)if not pending then local clearLabel,copyLabel=\"[Clear]\",\"[Copy]\"local clearX,copyX=width-#clearLabel-1,width-#clearLabel-#copyLabel-3 terminalUi.write(copyX,top,copyLabel,colors.lightBlue,colors.gray)terminalUi.write(clearX,top,clearLabel,colors.lightBlue,colors.gray)targets[#targets+1]={kind=\"item\",item=system(\"remote_result_copy\",\"Copy remote result\"),x1=copyX,x2=copyX+#copyLabel-1,y1=top,y2=top}targets[#targets+1]={kind=\"item\",item=system(\"remote_result_clear\",\"Clear remote result\"),x1=clearX,x2=clearX+#clearLabel-1,y1=top,y2=top}end if top+1<height then terminalUi.write(2,top+1,result.text or result.message or result.value or\"\",failed and colors.red or colors.lightGray,colors.black,width-3)end return top+2 end local function drawMenu(menu,width,height)if not menu or#menu.items==0 then return{}end local longest=0 for _,item in ipairs(menu.items)do longest=math.max(longest,#terminalUi.clean(item.label))end local boxWidth,x=math.max(8,math.min(width-2,longest+4)),math.max(1,width-math.max(8,math.min(width-2,longest+4)))local y=math.max(2,height-#menu.items-1)local targets={}for index,item in ipairs(menu.items)do local selected=index==menu.selected terminalUi.fill(x,y+index-1,boxWidth,1,selected and colors.gray or colors.black)terminalUi.write(x+1,y+index-1,item.label,selected and colors.white or colors.lightGray,selected and colors.gray or colors.black,boxWidth-2)targets[#targets+1]={kind=\"menu\",item=item,x1=x,x2=x+boxWidth-1,y1=y+index-1,y2=y+index-1}end return targets end local function drawDesktop(model,state,banner,width,height)if width<24 or height<9 then return terminalUi.minimum(model.title,\"Desktop needs 24x9\",\"Resize | Backspace\")end local top=drawHeader(model,banner,width,height)+1 local columns=math.max(2,math.floor((width-2)/14))local cellWidth,cellHeight=math.floor((width-2)/columns),4 local rows,capacity=math.max(1,math.floor((height-top-1)/cellHeight)),9 capacity=math.min(capacity,columns*rows)model.columns=columns local item,selectedIndex=M.selected(model,state)local totalPages=math.max(1,math.ceil(#model.items/capacity))state.desktopPage=math.max(1,math.min(math.floor((selectedIndex-1)/capacity)+1,totalPages))local first,last=(state.desktopPage-1)*capacity+1,math.min(#model.items,state.desktopPage*capacity)local targets={}for index=first,last do local slot,entry=index-first,model.items[index]local x,y=2+(slot%columns)*cellWidth,top+math.floor(slot/columns)*cellHeight local selected=entry.id==state.selected terminalUi.fill(x,y,cellWidth,3,selected and colors.gray or colors.black)terminalUi.write(x+1,y+1,entry.label,colour(entry),selected and colors.gray or colors.black,cellWidth-2)targets[#targets+1]={kind=\"item\",item=entry,x1=x,x2=x+cellWidth-1,y1=y,y2=y+2}end terminalUi.write(2,height,\"Arrows select | Enter open | M menu | [\"..state.desktopPage..\"/\"..totalPages..\"] | Q close\",colors.gray,colors.black,width-2)return targets end local function drawList(model,state,banner,width,height)if width<24 or height<8 then return terminalUi.minimum(model.title,\"Screen needs 24x8\",\"Resize | Backspace\")end local targets={}local top,footer=drawHeader(model,banner,width,height)+1,height-1 if model.page==\"peripherals\"then top=drawRemoteResult(model.remoteResult,top,width,footer,targets)+1 end for _,line in ipairs(model.lines or{})do if top>=footer-2 then break end terminalUi.write(2,top,line,colors.lightGray,colors.black,width-2);top=top+1 end local _,selectedIndex=M.selected(model,state)selectedIndex=selectedIndex or 1 local visible=math.max(1,footer-top)local first=math.max(1,math.min(math.max(1,#model.items-visible+1),selectedIndex-math.floor(visible/2)))model.columns=1 for row=0,visible-1 do local entry=model.items[first+row]if not entry then break end local y,selected=top+row,entry.id==state.selected terminalUi.clearLine(y,selected and colors.gray or colors.black)terminalUi.write(3,y,entry.label,colour(entry),selected and colors.gray or colors.black,width-4)targets[#targets+1]={kind=\"item\",item=entry,x1=1,x2=width,y1=y,y2=y}end local footerText if model.page==\"files\"then footerText=model.trash and\"Enter select | M menu | Backspace Explorer | E empty Trash\"or\"Enter open | M menu | N new folder | T Trash | Backspace up\"else footerText=\"Arrows select | Enter open | Backspace desktop | Q close\"end terminalUi.write(2,height,footerText,colors.gray,colors.black,width-2)return targets end local function drawPeripheralMatrix(model,state,banner,width,height)if width<36 or height<15 then return terminalUi.minimum(model.title,\"Devices matrix needs 36x15\",\"Resize | Backspace\")end local targets={}local top,footer=drawHeader(model,banner,width,height)+1,height-1 for _,line in ipairs(model.lines or{})do if top>=footer-1 then break end terminalUi.write(2,top,line,colors.lightGray,colors.black,width-2);top=top+1 end top=drawRemoteResult(model.remoteResult,top,width,footer,targets)+1 local devices,byKey={},{}for _,item in ipairs(model.items)do if item.id:sub(1,17)==\"peripheral_check:\"then if not byKey[item.name]then devices[#devices+1]={name=item.name,type=item.type or\"unknown\"};byKey[item.name]={}end byKey[item.name][item.mode]=item end end local controls={}for _,item in ipairs(model.items)do if item.kind==\"system\"and(item.action==\"share_all:peer\"or item.action==\"share_all:master\"or item.action==\"unshare_all\"or item.action==\"remote_cache\"or item.action==\"remote_call\")then controls[#controls+1]=item end end local nameX,masterX,peerX=2,width-11,width-17 local typeX=math.max(14,math.floor(width*0.45))local deviceRows=math.max(1,footer-top-#controls-2)local selectedDevice for index,device in ipairs(devices)do local row=byKey[device.name]if(row.peer and row.peer.id==state.selected)or(row.master and row.master.id==state.selected)then selectedDevice=index;break end end if selectedDevice then state.peripheralScroll=math.max(1,math.min(math.max(1,#devices-deviceRows+1),selectedDevice-math.floor(deviceRows/2)))end state.peripheralScroll=math.max(1,math.min(math.max(1,#devices-deviceRows+1),state.peripheralScroll or 1))terminalUi.write(nameX,top,\"Name\",colors.lightGray,colors.black,typeX-nameX-1)terminalUi.write(typeX,top,\"Type\",colors.lightGray,colors.black,peerX-typeX-1)terminalUi.write(peerX,top,\"Peer\",colors.lightGray,colors.black,5)terminalUi.write(masterX,top,\"Master\",colors.lightGray,colors.black,7)for row=0,deviceRows-1 do local device=devices[state.peripheralScroll+row]if not device then break end local y=top+row+1 terminalUi.write(nameX,y,device.name,colors.white,colors.black,typeX-nameX-1)terminalUi.write(typeX,y,device.type,colors.lightGray,colors.black,peerX-typeX-1)for _,mode in ipairs({\"peer\",\"master\"})do local item=byKey[device.name][mode]local selected,x=item and state.selected==item.id,mode==\"peer\"and peerX or masterX terminalUi.write(x,y,item and(item.checked and\"[x]\"or\"[ ]\")or\"   \",item and item.checked and colors.lime or colors.lightGray,selected and colors.gray or colors.black,3)if item then targets[#targets+1]={kind=\"item\",item=item,x1=x,x2=x+2,y1=y,y2=y}end end end local y=top+deviceRows+1 for _,item in ipairs(controls)do if y>=footer then break end local selected=state.selected==item.id terminalUi.clearLine(y,selected and colors.gray or colors.black)terminalUi.write(2,y,item.label,selected and colors.white or colors.lightGray,selected and colors.gray or colors.black,width-2)targets[#targets+1]={kind=\"item\",item=item,x1=1,x2=width,y1=y,y2=y}y=y+1 end terminalUi.write(2,height,\"Arrows select | Enter toggle | Click checkbox | Backspace devices\",colors.gray,colors.black,width-2)return targets end function M.draw(model,state,banner)local width,height=term.getSize()local targets=model.desktop and drawDesktop(model,state,banner,width,height)or(model.page==\"peripherals\"and state.peripheralView and drawPeripheralMatrix(model,state,banner,width,height)or drawList(model,state,banner,width,height))local menuTargets=drawMenu(state.menu,width,height)for _,target in ipairs(menuTargets)do targets[#targets+1]=target end term.setBackgroundColor(colors.black);term.setTextColor(colors.white)return targets end function M.actionFor(model,number)if model.page==\"peers\"and number==2 then return{id=\"pair_offer\",label=\"Start discovery\"}end return model.items[number]end return M",
  ["ceetos/lib/terminal_ui.lua"] = "local M={}local function size()local width,height=term.getSize()return math.max(1,width or 1),math.max(1,height or 1)end function M.clean(value)return tostring(value or\"\"):gsub(\"%c\",\" \")end function M.clip(value,width,suffix)value,width=M.clean(value),math.max(0,math.floor(tonumber(width)or 0))if width==0 then return\"\"end if#value<=width then return value end suffix=suffix==nil and\"...\"or tostring(suffix)if#suffix>=width then return suffix:sub(1,width)end return value:sub(1,width-#suffix)..suffix end function M.write(x,y,value,foreground,background,width)local terminalWidth,terminalHeight=size()x,y=math.floor(tonumber(x)or 1),math.floor(tonumber(y)or 1)if y<1 or y>terminalHeight or x>terminalWidth then return false end if x<1 then value=M.clean(value):sub(2-x)x=1 end local available=math.max(0,terminalWidth-x+1)if width~=nil then available=math.min(available,math.max(0,math.floor(width)))end if available<=0 then return false end if foreground then term.setTextColor(foreground)end if background then term.setBackgroundColor(background)end term.setCursorPos(x,y)term.write(M.clip(value,available,\"\"))return true end function M.clearLine(y,background)local width,height=size()y=math.floor(tonumber(y)or 0)if y<1 or y>height then return false end term.setCursorPos(1,y)if background then term.setBackgroundColor(background)end term.clearLine()return width>0 end function M.fill(x,y,width,height,background)local terminalWidth,terminalHeight=size()x,y=math.max(1,math.floor(tonumber(x)or 1)),math.max(1,math.floor(tonumber(y)or 1))width,height=math.max(0,math.floor(tonumber(width)or 0)),math.max(0,math.floor(tonumber(height)or 0))width,height=math.min(width,terminalWidth-x+1),math.min(height,terminalHeight-y+1)if width<=0 or height<=0 then return false end term.setBackgroundColor(background or colors.black)for row=y,y+height-1 do term.setCursorPos(x,row);term.write((\" \"):rep(width))end return true end function M.box(x1,y1,x2,y2,title,border,titleColour)local width,height=size()x1,y1=math.max(1,math.floor(x1 or 1)),math.max(1,math.floor(y1 or 1))x2,y2=math.min(width,math.floor(x2 or width)),math.min(height,math.floor(y2 or height))if x2-x1<2 or y2-y1<2 then return false end border=border or colors.gray term.setBackgroundColor(colors.black);term.setTextColor(border)M.write(x1,y1,\"+\"..(\"-\"):rep(x2-x1-1)..\"+\",border,colors.black)for row=y1+1,y2-1 do M.write(x1,row,\"|\",border,colors.black);M.write(x2,row,\"|\",border,colors.black)end M.write(x1,y2,\"+\"..(\"-\"):rep(x2-x1-1)..\"+\",border,colors.black)if title and title~=\"\"then M.write(x1+2,y1,\" \"..M.clean(title)..\" \",titleColour or colors.lightBlue,colors.black,math.max(0,x2-x1-3))end return true end function M.minimum(title,message,footer)local width,height=size()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()M.clearLine(1,colors.blue);M.write(2,1,title or\"CeetOS\",colors.white,colors.blue,width-2)M.write(2,math.max(2,math.floor(height/2)),message or\"Terminal is too small\",colors.red,colors.black,width-2)M.write(2,height,footer or\"Resize terminal or press Backspace\",colors.gray,colors.black,width-2)return{}end function M.hit(targets,x,y)for index=#(targets or{}),1,-1 do local target=targets[index]if x>=target.x1 and x<=target.x2 and y>=target.y1 and y<=target.y2 then return target end end end return M",
  ["ceetos/lib/files.lua"] = "local store=require(\"ceetos.lib.store\")local auth=require(\"ceetos.lib.auth\")local M={}local TRASH_DIR=\"/ceetos/data/trash\"local TRASH_INDEX=\"/ceetos/data/trash-index.lua\"local protectedRoots={\"/ceetos\",\"/rom\"}local protectedFiles={[\"/startup.lua\"]=true}local function absolute(path)if not path or path==\"\"or path==\"/\"then return\"/\"end return path:sub(1,1)==\"/\"and path or\"/\"..path end local function sort(items)table.sort(items,function(a,b)if a.directory~=b.directory then return a.directory end return a.name:lower()<b.name:lower()end)return items end local function availablePath(directory,name)name=fs.getName(name)local base,extension=name:match(\"^(.*)(%.[^.]*)$\")base,extension=base or name,extension or\"\"local path,index=fs.combine(directory,name),2 while fs.exists(path)do path=fs.combine(directory,base..\"-\"..index..extension)index=index+1 end return absolute(path)end function M.isProtected(path)path=absolute(path)if protectedFiles[path]then return true end for _,root in ipairs(protectedRoots)do if path==root or path:sub(1,#root+1)==root..\"/\"then return true end end return false end function M.describe(path)path=absolute(path)return{name=fs.getName(path),path=path,directory=fs.exists(path)and fs.isDir(path)or false,runnable=fs.exists(path)and not fs.isDir(path)and path:sub(-4):lower()==\".lua\",protected=M.isProtected(path),}end function M.list(path)path=absolute(path==\"\"and\"/\"or(path or\"/\"))local result={}if path~=\"/\"then local parent=absolute(fs.getDir(path))result[#result+1]={name=\"..\",path=parent,directory=true,parent=true,protected=M.isProtected(parent)}end for _,name in ipairs(fs.list(path))do result[#result+1]=M.describe(absolute(fs.combine(path,name)))end return sort(result)end local function collect(path,depth,result)if depth<0 or not fs.exists(path)or not fs.isDir(path)then return end for _,item in ipairs(M.list(path))do if not item.parent then if item.directory then collect(item.path,depth-1,result)elseif item.runnable then result[#result+1]=item end end end end function M.programs()local result={}collect(\"/programs\",2,result)return result end function M.runnable(path)return M.describe(path).runnable end function M.path(directory,name)return absolute(fs.combine(directory,fs.getName(name)))end local function assertMutable(path)path=absolute(path)assert(path~=\"/\"and fs.exists(path),\"file not found\")assert(not M.isProtected(path),\"CeetOS system files cannot be changed from Explorer\")return path end function M.rename(path,name)path=assertMutable(path)assert(name and fs.getName(name)==name and name~=\"\",\"invalid file name\")local target=M.path(fs.getDir(path),name)assert(not M.isProtected(target),\"CeetOS system files cannot be changed from Explorer\")assert(not fs.exists(target),\"a file with that name already exists\")fs.move(path,target)return target end function M.makeDirectory(directory,name)directory=absolute(directory)assert(not M.isProtected(directory),\"CeetOS system folders cannot be changed from Explorer\")assert(name and fs.getName(name)==name and name~=\"\",\"invalid folder name\")local target=M.path(directory,name)assert(not fs.exists(target),\"a file with that name already exists\")fs.makeDir(target)return target end function M.readText(path)path=absolute(path)assert(fs.exists(path)and not fs.isDir(path),\"file not found\")local handle=assert(fs.open(path,\"r\"))local contents=handle.readAll()handle.close()return contents end function M.writeText(path,contents)path=absolute(path)assert(not M.isProtected(path),\"CeetOS system files cannot be changed from Editor\")local parent=absolute(fs.getDir(path))assert(fs.exists(parent)and fs.isDir(parent),\"parent folder does not exist\")local temporary,backup=path..\".ceetos-tmp\",path..\".ceetos-backup\"if fs.exists(temporary)then fs.delete(temporary)end if fs.exists(backup)then fs.delete(backup)end local handle=assert(fs.open(temporary,\"w\"));handle.write(contents);handle.close()if fs.exists(path)then fs.move(path,backup)end local ok,err=pcall(fs.move,temporary,path)if not ok then if fs.exists(backup)then fs.move(backup,path)end error(err,0)end if fs.exists(backup)then fs.delete(backup)end return path end local function trashIndex()return store.read(TRASH_INDEX,{})end local function saveTrashIndex(index)store.write(TRASH_INDEX,index)end function M.moveToTrash(path)path=assertMutable(path)if not fs.exists(TRASH_DIR)then fs.makeDir(TRASH_DIR)end local index=trashIndex()local id=tostring(os.epoch(\"utc\"))..\"-\"..tostring(math.random(1000,9999))while index[id]do id=id..\"x\"end local stored=absolute(fs.combine(TRASH_DIR,id))fs.move(path,stored)index[id]={original=path,name=fs.getName(path),directory=fs.isDir(stored),deletedAt=os.epoch(\"utc\")}saveTrashIndex(index)return id end function M.trashList()local index,result=trashIndex(),{}for id,entry in pairs(index)do local stored=absolute(fs.combine(TRASH_DIR,id))if fs.exists(stored)then result[#result+1]={id=id,path=stored,name=entry.name,original=entry.original,directory=entry.directory,trashed=true,deletedAt=entry.deletedAt,runnable=not entry.directory and entry.name:sub(-4):lower()==\".lua\"}else index[id]=nil end end saveTrashIndex(index)table.sort(result,function(a,b)return(a.deletedAt or 0)>(b.deletedAt or 0)end)return result end function M.restore(id)local index=trashIndex()local entry=index[id]assert(entry,\"trash item not found\")local stored=absolute(fs.combine(TRASH_DIR,id))assert(fs.exists(stored),\"trash item no longer exists\")local parent=absolute(fs.getDir(entry.original))assert(not M.isProtected(entry.original),\"cannot restore into a protected system path\")if not fs.exists(parent)then fs.makeDir(parent)end local target=availablePath(parent,fs.getName(entry.original))fs.move(stored,target)index[id]=nil;saveTrashIndex(index)return target end function M.deleteTrash(id)local index,entry=trashIndex(),trashIndex()[id]assert(entry,\"trash item not found\")local stored=absolute(fs.combine(TRASH_DIR,id))if fs.exists(stored)then fs.delete(stored)end index[id]=nil;saveTrashIndex(index)end function M.emptyTrash()if fs.exists(TRASH_DIR)then fs.delete(TRASH_DIR)end saveTrashIndex({})end function M.import(transfers,directory)auth.require(\"operator\")directory=directory or\"/programs/imports\"if not fs.exists(directory)then fs.makeDir(directory)end local imported={}for _,transfer in ipairs(transfers or{})do local name=transfer.getName and transfer.getName()if name and name~=\"\"then local path=availablePath(directory,name)local handle=assert(fs.open(path,\"wb\"))while true do local chunk=transfer.read(8192)if not chunk then break end handle.write(chunk)end handle.close()imported[#imported+1]=path end end return imported end return M",
  ["ceetos/lib/editor.lua"] = "local M={}local selection=require(\"ceetos.lib.selection\")local function clamp(value,low,high)return math.max(low,math.min(high,value))end local function split(text)text=(text or\"\"):gsub(\"\\r\\n\",\"\\n\"):gsub(\"\\r\",\"\\n\")local lines={}for line in(text..\"\\n\"):gmatch(\"(.-)\\n\")do lines[#lines+1]=line end if#lines==0 then lines[1]=\"\"end return lines end function M.new(text)local lines=split(text)return{lines=lines,line=1,column=0,changed=false,preferredColumn=nil,selection=selection.new(lines)}end function M.text(buffer)return table.concat(buffer.lines,\"\\n\")end function M.current(buffer)return buffer.lines[buffer.line]end function M.clampCursor(buffer)buffer.line=clamp(buffer.line,1,#buffer.lines)buffer.column=clamp(buffer.column,0,#buffer.lines[buffer.line])end function M.move(buffer,lineDelta,columnDelta)buffer.line=clamp(buffer.line+(lineDelta or 0),1,#buffer.lines)buffer.column=clamp((buffer.preferredColumn or buffer.column)+(columnDelta or 0),0,#buffer.lines[buffer.line])if lineDelta and lineDelta~=0 and columnDelta==0 then buffer.column=clamp(buffer.preferredColumn or buffer.column,0,#buffer.lines[buffer.line])end buffer.preferredColumn=buffer.column end function M.gotoLine(buffer,line,column)buffer.line=clamp(tonumber(line)or 1,1,#buffer.lines)buffer.column=clamp(tonumber(column)or 0,0,#buffer.lines[buffer.line])buffer.preferredColumn=buffer.column end function M.selectStart(buffer)buffer.selection.lines=buffer.lines;selection.start(buffer.selection,buffer.line,buffer.column);return buffer end function M.selectTo(buffer,line,column)buffer.selection.lines=buffer.lines;selection.update(buffer.selection,line,column);return buffer end function M.clearSelection(buffer)selection.clear(buffer.selection);return buffer end function M.selectionText(buffer)buffer.selection.lines=buffer.lines;return selection.text(buffer.selection)end function M.copySelection(buffer,clipboard)buffer.selection.lines=buffer.lines;return selection.copy(buffer.selection,clipboard)end function M.deleteSelection(buffer)local range=selection.range(buffer.selection);if not range then return false end local first,last=range.first,range.last local startText=(buffer.lines[first.line]or\"\"):sub(1,first.column)local endText=(buffer.lines[last.line]or\"\"):sub(last.column+1)buffer.lines[first.line]=startText..endText for _=first.line+1,last.line do table.remove(buffer.lines,first.line+1)end buffer.line,buffer.column,buffer.preferredColumn=first.line,first.column,first.column buffer.selection.lines=buffer.lines;selection.clear(buffer.selection);buffer.changed=true;return true end function M.insert(buffer,value)value=value or\"\"if selection.range(buffer.selection)then M.deleteSelection(buffer)end local before,after=M.current(buffer):sub(1,buffer.column),M.current(buffer):sub(buffer.column+1)local parts=split(value)if#parts==1 then buffer.lines[buffer.line]=before..value..after buffer.column=buffer.column+#value else buffer.lines[buffer.line]=before..parts[1]for index=2,#parts-1 do table.insert(buffer.lines,buffer.line+index-1,parts[index])end table.insert(buffer.lines,buffer.line+#parts-1,parts[#parts]..after)buffer.line,buffer.column=buffer.line+#parts-1,#parts[#parts]end buffer.changed,buffer.preferredColumn=true,buffer.column end function M.backspace(buffer)if buffer.column>0 then local line=M.current(buffer)buffer.lines[buffer.line]=line:sub(1,buffer.column-1)..line:sub(buffer.column+1)buffer.column=buffer.column-1 elseif buffer.line>1 then local previous=buffer.lines[buffer.line-1]buffer.lines[buffer.line-1]=previous..M.current(buffer)table.remove(buffer.lines,buffer.line);buffer.line,buffer.column=buffer.line-1,#previous else return false end buffer.changed,buffer.preferredColumn=true,buffer.column;return true end function M.delete(buffer)local line=M.current(buffer)if buffer.column<#line then buffer.lines[buffer.line]=line:sub(1,buffer.column)..line:sub(buffer.column+2)elseif buffer.line<#buffer.lines then buffer.lines[buffer.line]=line..buffer.lines[buffer.line+1];table.remove(buffer.lines,buffer.line+1)else return false end buffer.changed=true;return true end function M.find(buffer,needle,startLine)if not needle or needle==\"\"then return nil end for line=startLine or buffer.line,#buffer.lines do local column=buffer.lines[line]:find(needle,line==buffer.line and buffer.column+1 or 1,true)if column then buffer.line,buffer.column,buffer.preferredColumn=line,column-1,column-1;return line,column-1 end end return nil end local keywords={[\"and\"]=true,[\"break\"]=true,[\"do\"]=true,[\"else\"]=true,[\"elseif\"]=true,[\"end\"]=true,[\"false\"]=true,[\"for\"]=true,[\"function\"]=true,[\"if\"]=true,[\"in\"]=true,[\"local\"]=true,[\"nil\"]=true,[\"not\"]=true,[\"or\"]=true,[\"repeat\"]=true,[\"return\"]=true,[\"then\"]=true,[\"true\"]=true,[\"until\"]=true,[\"while\"]=true}local builtins={print=true,pairs=true,ipairs=true,tonumber=true,tostring=true,type=true,error=true,assert=true,fs=true,term=true,shell=true,peripheral=true,os=true,textutils=true}function M.tokens(line,lua)if not lua then return{{text=line,kind=\"text\"}}end local out,index={},1 local function add(text,kind)if text~=\"\"then out[#out+1]={text=text,kind=kind}end end while index<=#line do local rest=line:sub(index)if rest:sub(1,2)==\"--\"then add(rest,\"comment\");break end local quote=rest:sub(1,1)if quote==\"'\"or quote=='\"'then local finish,escaped=2,false while finish<=#rest do if rest:sub(finish,finish)==quote and not escaped then break end;escaped=rest:sub(finish,finish)==\"\\\\\"and not escaped;if rest:sub(finish,finish)~=\"\\\\\"then escaped=false end;finish=finish+1 end add(rest:sub(1,finish),\"string\");index=index+finish else local word=rest:match(\"^[%a_][%w_]*\")local number=rest:match(\"^%d+%.?%d*\")if word then add(word,keywords[word]and\"keyword\"or(builtins[word]and\"builtin\"or\"text\"));index=index+#word elseif number then add(number,\"number\");index=index+#number else add(rest:sub(1,1),\"text\");index=index+1 end end end return out end return M",
  ["ceetos/lib/terminal.lua"] = "local store=require(\"ceetos.lib.store\")local M,PATH,LIMIT={},\"/ceetos/data/terminal-history.lua\",50 function M.history()return store.read(PATH,{})end function M.add(command)if not command or command==\"\"then return end local history=M.history()if history[#history]~=command then history[#history+1]=command end while#history>LIMIT do table.remove(history,1)end store.write(PATH,history)end function M.complete(prefix,candidates)local result,seen={},{}for _,candidate in ipairs(candidates or{})do if candidate:sub(1,#prefix)==prefix and not seen[candidate]then result[#result+1],seen[candidate]=candidate,true end end table.sort(result);return result end return M",
  ["ceetos/lib/selection.lua"] = "local M={}local function point(line,column)return{line=math.max(1,tonumber(line)or 1),column=math.max(0,tonumber(column)or 0)}end local function before(a,b)return a.line<b.line or(a.line==b.line and a.column<=b.column)end local function norm(a,b)return before(a,b)and a or b,before(a,b)and b or a end function M.new(lines)return{lines=lines or{\"\"},anchor=nil,active=nil}end function M.position(line,column,maxLine,maxColumn)line=math.max(1,math.min(tonumber(maxLine)or line or 1,tonumber(line)or 1))column=math.max(0,tonumber(column)or 0)if maxColumn and line==maxLine then column=math.min(column,maxColumn)end return line,column end function M.start(s,line,column)s.anchor,s.active=point(line,column),point(line,column);return s end function M.update(s,line,column)if s.anchor then s.active=point(line,column)end;return s end function M.clear(s)s.anchor,s.active=nil,nil;return s end function M.range(s)if not s.anchor or not s.active then return nil end local first,last=norm(s.anchor,s.active)if first.line==last.line and first.column==last.column then return nil end return{first=first,last=last}end function M.text(s)local r=M.range(s);if not r then return\"\"end local out={}for line=r.first.line,r.last.line do local value=s.lines[line]or\"\"local from=line==r.first.line and r.first.column+1 or 1 local to=line==r.last.line and r.last.column or#value out[#out+1]=value:sub(from,to)end return table.concat(out,\"\\n\")end function M.copy(s,clipboard)local text=M.text(s);if text~=\"\"and clipboard then clipboard.set(text)end;return text end return M",
  ["ceetos/lib/clipboard.lua"] = "local store=require(\"ceetos.lib.store\")local M,PATH,OUTBOX,LIMIT={},\"/ceetos/data/clipboard.lua\",\"/ceetos-dev/clipboard-out.lua\",16384 local memory local function trim(value)return tostring(value or\"\"):sub(1,LIMIT)end function M.get()if memory~=nil then return memory end local value=store.read(PATH,{text=\"\"})memory=value and value.text or\"\"return memory end function M.set(value)local text=trim(value)memory=text pcall(store.write,PATH,{text=text})if fs and fs.makeDir and fs.open then pcall(function()if not fs.exists(\"/ceetos-dev\")then pcall(fs.makeDir,\"/ceetos-dev\")end local handle=fs.open(OUTBOX,\"w\")if handle then handle.write(textutils.serialize({text=text,queued=os.epoch and os.epoch(\"utc\")or 0}));handle.close()end end)end return text end function M.clear()return M.set(\"\")end function M.copy(value)return M.set(value)end function M.limit()return LIMIT end return M",
  ["ceetos/lib/input.lua"] = "local M={}function M.modifiers()return{ctrl=false,shift=false}end local function nameOf(name)name=tostring(name or\"\")local lower=name:lower()if lower==\"leftctrl\"or lower==\"rightctrl\"or lower==\"ctrl\"then return\"ctrl\"end if lower==\"leftshift\"or lower==\"rightshift\"or lower==\"shift\"then return\"shift\"end return lower end function M.key(state,name)name=nameOf(name)if name==\"ctrl\"then state.ctrl=true elseif name==\"shift\"then state.shift=true end return state end function M.keyUp(state,name)name=nameOf(name)if name==\"ctrl\"then state.ctrl=false elseif name==\"shift\"then state.shift=false end return state end function M.shortcut(state,name)name=nameOf(name)local active=state.ctrl and state.shift and(name==\"c\"or name==\"v\")if active then state.ctrl,state.shift=false,false end return active,name end return M",
  ["ceetos/lib/version.lua"] = "return{major=0,minor=16,patch=3,string=\"0.16.3\"}",
  ["ceetos/lib/crafting.lua"] = "local M={}M.RECIPE_PACK_MAX_BYTES=64*1024 function M.isLocalPeer(peer,computerId)return tostring(peer)==tostring(computerId)end function M.normalise(rows)local out={}for _,row in pairs(type(rows)==\"table\"and rows or{})do if type(row)==\"table\"then local name,count=row.name or row.id,tonumber(row.count or row.amount or row.quantity or 0)or 0 if type(name)==\"string\"and count>0 then out[name]=(out[name]or 0)+math.floor(count)end end end return out end function M.rows(stock,query,filter,recipes)local output,needle,available={},tostring(query or\"\"):lower(),M.normalise(stock)local names={}for name in pairs(available)do names[name]=true end for name in pairs(type(recipes)==\"table\"and recipes or{})do names[name]=true end for name in pairs(names)do local count=available[name]or 0 local craftable=type(recipes)==\"table\"and recipes[name]~=nil local include=needle==\"\"or name:lower():find(needle,1,true)if filter==\"craftable\"then include=include and craftable elseif filter==\"stocked\"then include=include and count>0 end if include then output[#output+1]={name=name,count=count,craftable=craftable,stocked=count>0}end end table.sort(output,function(a,b)return a.name<b.name end)return output end function M.addToCart(cart,name,count)if type(cart)~=\"table\"or type(name)~=\"string\"or name==\"\"then return nil,\"invalid cart item\"end count=tonumber(count)if not count or count<1 then return nil,\"invalid cart quantity\"end count=math.floor(count)for _,row in ipairs(cart)do if row.name==name then row.count=(tonumber(row.count)or 0)+count;return row end end local row={name=name,count=count}cart[#cart+1]=row return row end function M.cartTotals(cart)local items,quantity=0,0 for _,row in ipairs(type(cart)==\"table\"and cart or{})do items=items+1 quantity=quantity+math.max(0,math.floor(tonumber(row.count)or 0))end return items,quantity end function M.acceptedCount(result,requested)local accepted=tonumber(result)requested=tonumber(requested)if not accepted or not requested or requested<1 then return nil,\"invalid request result\"end accepted=math.floor(accepted)if accepted<1 then return nil,\"Stock Ticker accepted no items\"end return math.min(accepted,math.floor(requested))end function M.displayRow(name,count,width)local suffix=\" x\"..tostring(count)width=math.max(#suffix+1,math.floor(tonumber(width)or#suffix+1))if#name+#suffix<=width then return name..suffix end return name:sub(1,math.max(1,width-#suffix-1))..\"…\"..suffix end function M.cancellationStatus(issued)return issued and\"cancelled-unconfirmed\"or\"cancelled\"end local function validReference(name)return type(name)==\"string\"and name:match(\"^#?[%w_.:%-]+$\")~=nil and not name:find(\"##\",1,true)end local function recipeIngredients(recipe,batches)local combined={}local declared=type(recipe.ingredients)==\"table\"and#recipe.ingredients>0 if declared then for _,ingredient in ipairs(recipe.ingredients)do local name=type(ingredient)==\"table\"and ingredient.id or ingredient local count=type(ingredient)==\"table\"and tonumber(ingredient.count or 1)or 1 if validReference(name)then combined[name]=(combined[name]or 0)+batches*math.max(1,math.floor(count or 1))end end else for slot=1,9 do local name=recipe.grid and recipe.grid[slot]if name and name~=false then combined[name]=(combined[name]or 0)+batches end end end local out={}for name,count in pairs(combined)do out[#out+1]={name=name,count=count}end table.sort(out,function(a,b)return a.name<b.name end)return out end function M.recipeInputs(recipe,batches)return recipeIngredients(recipe or{},math.max(1,math.floor(tonumber(batches)or 1)))end function M.validateRecipes(recipes)if type(recipes)~=\"table\"then return nil,\"recipes must return a table\"end for name,recipe in pairs(recipes)do if type(name)~=\"string\"or not name:match(\"^[%w_.:%-]+$\")or type(recipe)~=\"table\"then return nil,\"invalid recipe item id\"end if type(recipe.alternatives)==\"table\"then if#recipe.alternatives<1 then return nil,\"recipe \"..name..\" has no alternatives\"end for _,alternative in ipairs(recipe.alternatives)do local valid,err=M.validateRecipes({[name]=alternative});if not valid then return nil,err end end else if recipe.mode~=\"crafting\"then return nil,\"recipe \"..name..\" must use mode = crafting\"end if type(recipe.address)~=\"string\"or recipe.address==\"\"or#recipe.address>64 then return nil,\"recipe \"..name..\" requires an address\"end local crafter=recipe.address==\"Crafter\"if crafter and type(recipe.grid)~=\"table\"then return nil,\"recipe \"..name..\" requires a 9-slot grid for Crafter\"end if not crafter and type(recipe.ingredients)~=\"table\"and type(recipe.grid)~=\"table\"then return nil,\"recipe \"..name..\" requires ingredients\"end local output=tonumber(recipe.output or 1)if not output or output<1 or output~=math.floor(output)then return nil,\"invalid output for \"..name end for slot=1,9 do local value=recipe.grid and recipe.grid[slot]if value~=nil and value~=false and not validReference(value)then return nil,\"invalid grid slot in \"..name end end for _,ingredient in ipairs(type(recipe.ingredients)==\"table\"and recipe.ingredients or{})do local id,count=type(ingredient)==\"table\"and ingredient.id or ingredient,type(ingredient)==\"table\"and ingredient.count or 1 if not validReference(id)or not tonumber(count)or tonumber(count)<1 then return nil,\"invalid ingredient in \"..name end end end end return recipes end local function dataParser(source,maximum)if type(source)~=\"string\"then return nil,\"recipe data must be text\"end if#source==0 or#source>(maximum or M.RECIPE_PACK_MAX_BYTES)then return nil,\"recipe data exceeds the 64 KiB import limit\"end local state={source=source,index=1,length=#source,depth=0,nodes=0}local function skip()while state.index<=state.length do local char=state.source:sub(state.index,state.index)if char:match(\"%s\")then state.index=state.index+1 elseif state.source:sub(state.index,state.index+1)==\"--\"then local ending=state.source:find(\"\\n\",state.index+2,true)state.index=ending and ending+1 or state.length+1 else return true end end return true end local function fail(message)return nil,message..\" near byte \"..tostring(state.index)end local function identifier()local name=state.source:match(\"^[%a_][%w_]*\",state.index)if name then state.index=state.index+#name end return name end local parseValue local function parseString()local quote=state.source:sub(state.index,state.index);state.index=state.index+1 local out={}while state.index<=state.length do local char=state.source:sub(state.index,state.index);state.index=state.index+1 if char==quote then return table.concat(out)end if char==\"\\\\\"then local escaped=state.source:sub(state.index,state.index);state.index=state.index+1 local values={n=\"\\n\",r=\"\\r\",t=\"\\t\",[\"\\\\\"]=\"\\\\\",[\"\\\"\"]=\"\\\"\",[\"'\"]=\"'\"}if not values[escaped]then return fail(\"unsupported string escape\")end out[#out+1]=values[escaped]elseif char:byte()<32 then return fail(\"control character in string\")else out[#out+1]=char end end return fail(\"unterminated string\")end local function parseTable()state.depth=state.depth+1 if state.depth>32 then return fail(\"recipe data is nested too deeply\")end state.index=state.index+1 local out,nextIndex,keys={},1,{}skip()while state.index<=state.length and state.source:sub(state.index,state.index)~=\"}\"do state.nodes=state.nodes+1 if state.nodes>4096 then return fail(\"recipe data has too many values\")end local key,value,err local start=state.index if state.source:sub(state.index,state.index)==\"[\"then state.index=state.index+1;key,err=parseValue();if err then return nil,err end skip();if state.source:sub(state.index,state.index)~=\"]\"then return fail(\"missing closing recipe key bracket\")end state.index=state.index+1;skip();if state.source:sub(state.index,state.index)~=\"=\"then return fail(\"missing recipe key assignment\")end state.index=state.index+1;skip();value,err=parseValue();if err then return nil,err end else local named=identifier();skip()if named and state.source:sub(state.index,state.index)==\"=\"then key,state.index=named,state.index+1;skip();value,err=parseValue();if err then return nil,err end else state.index=start;value,err=parseValue();if err then return nil,err end key,nextIndex=nextIndex,nextIndex+1 end end if type(key)~=\"string\"and type(key)~=\"number\"then return fail(\"recipe table key must be text or a number\")end local signature=type(key)..\":\"..tostring(key)if keys[signature]then return fail(\"duplicate recipe table key\")end keys[signature],out[key]=true,value skip();local separator=state.source:sub(state.index,state.index)if separator==\",\"or separator==\";\"then state.index=state.index+1;skip()elseif separator~=\"}\"then return fail(\"expected recipe table separator\")end end if state.source:sub(state.index,state.index)~=\"}\"then return fail(\"unterminated recipe table\")end state.index,state.depth=state.index+1,state.depth-1 return out end parseValue=function()skip();local char=state.source:sub(state.index,state.index)if char==\"{\"then return parseTable()end if char==\"\\\"\"or char==\"'\"then return parseString()end local number=state.source:match(\"^-?%d+\",state.index)if number then state.index=state.index+#number;return tonumber(number)end local name=identifier()if name==\"true\"then return true elseif name==\"false\"then return false elseif name==\"nil\"then return nil elseif name then return fail(\"bare recipe values are not allowed\")end return fail(\"invalid recipe value\")end skip()if identifier()~=\"return\"then return fail(\"recipe data must start with return\")end local value,err=parseValue();if err then return nil,err end skip();if state.index<=state.length then return fail(\"unexpected content after recipe table\")end return value end local function validTags(tags)if tags==nil then return{}end if type(tags)~=\"table\"then return nil,\"recipe tags must be a table\"end local out={}for tag,members in pairs(tags)do local key=tostring(tag):gsub(\"^#\",\"\")if not key:match(\"^[%w_.:%-]+$\")or type(members)~=\"table\"or#members<1 then return nil,\"invalid recipe tag \"..tostring(tag)end out[key]={}for _,member in ipairs(members)do if not validReference(member)or tostring(member):sub(1,1)==\"#\"then return nil,\"invalid member of #\"..key end out[key][#out[key]+1]=member end table.sort(out[key])end return out end function M.validateRecipeGraph(recipes,tags)local tagMap,tagErr=validTags(tags);if not tagMap then return nil,tagErr end local visiting,complete={},{}local function visit(name)if complete[name]then return true end if visiting[name]then return nil,\"recipe cycle at \"..name end local recipe=recipes[name]if type(recipe)==\"table\"and type(recipe.alternatives)==\"table\"then recipe=recipe.alternatives[1]end if not recipe then return true end visiting[name]=true for _,input in ipairs(recipeIngredients(recipe,1))do local ingredient=input.name local members=ingredient:sub(1,1)==\"#\"and tagMap[ingredient:sub(2)]or{ingredient}for _,member in ipairs(members or{})do if recipes[member]then local ok,err=visit(member);if not ok then return nil,err end end end end visiting[name],complete[name]=nil,true return true end for name in pairs(recipes)do local ok,err=visit(name);if not ok then return nil,err end end return true end function M.parseRecipeOverlay(source)local value,err=dataParser(source,M.RECIPE_PACK_MAX_BYTES)if not value then return nil,err end if type(value)~=\"table\"then return nil,\"recipe overlay must return a table\"end local recipes,tags if value.schema==1 and type(value.recipes)==\"table\"then recipes,tags=value.recipes,value.tags or{}else recipes,tags=value,{}end local valid,validationErr=M.validateRecipes(recipes)if not valid then return nil,validationErr end local tagMap,tagsErr=validTags(tags);if not tagMap then return nil,tagsErr end local graphOk,graphErr=M.validateRecipeGraph(recipes,tagMap)if not graphOk then return nil,graphErr end return{schema=1,recipes=recipes,tags=tagMap}end function M.parseRecipeFile(source)local overlay,err=M.parseRecipeOverlay(source)return overlay and overlay.recipes or nil,err end function M.parseRecipePack(source)local pack,err=dataParser(source,M.RECIPE_PACK_MAX_BYTES)if not pack then return nil,err end if type(pack)~=\"table\"or pack.schema~=1 or type(pack.recipes)~=\"table\"then return nil,\"recipe pack must return { schema = 1, recipes = { ... } }\"end local valid,validationErr=M.validateRecipes(pack.recipes)if not valid then return nil,validationErr end local tagMap,tagsErr=validTags(pack.tags or{});if not tagMap then return nil,tagsErr end local graphOk,graphErr=M.validateRecipeGraph(pack.recipes,tagMap)if not graphOk then return nil,graphErr end return{schema=1,recipes=pack.recipes,tags=tagMap}end function M.recipeEqual(left,right)if type(left)~=\"table\"or type(right)~=\"table\"then return false end if type(left.alternatives)==\"table\"then left=left.alternatives[1]end if type(right.alternatives)==\"table\"then right=right.alternatives[1]end if not left or not right or left.mode~=right.mode or left.kind~=right.kind or left.output~=right.output or left.address~=right.address then return false end for slot=1,9 do if left.grid and right.grid and left.grid[slot]~=right.grid[slot]then return false end end local a,b=recipeIngredients(left,1),recipeIngredients(right,1)if#a~=#b then return false end for index=1,#a do if a[index].name~=b[index].name or a[index].count~=b[index].count then return false end end return true end function M.reviewRecipePack(existing,incoming)local validExisting,existingErr=M.validateRecipes(existing)if not validExisting then return nil,existingErr end local validIncoming,incomingErr=M.validateRecipes(incoming)if not validIncoming then return nil,incomingErr end local rows,counts={},{new=0,unchanged=0,conflict=0,invalid=0}for name,recipe in pairs(incoming)do local prior=existing[name]local status=not prior and\"new\"or(M.recipeEqual(prior,recipe)and\"unchanged\"or\"conflict\")counts[status]=counts[status]+1 rows[#rows+1]={name=name,status=status,incoming=recipe,existing=prior,selected=status==\"new\"}end table.sort(rows,function(a,b)return a.name<b.name end)return{rows=rows,counts=counts}end local function cloneRecipe(recipe)if type(recipe)==\"table\"and type(recipe.alternatives)==\"table\"then recipe=recipe.alternatives[1]end local copy={mode=recipe.mode,kind=recipe.kind,output=recipe.output,address=recipe.address,grid={},ingredients={}}for slot=1,9 do copy.grid[slot]=recipe.grid and recipe.grid[slot]end for _,input in ipairs(recipe.ingredients or{})do copy.ingredients[#copy.ingredients+1]={id=input.id,count=input.count,tag=input.tag}end return copy end function M.mergeRecipePack(existing,review,existingTags,incomingTags)if type(existing)~=\"table\"or type(review)~=\"table\"or type(review.rows)~=\"table\"then return nil,\"invalid recipe import review\"end local merged,imported={},0 for name,recipe in pairs(existing)do merged[name]=cloneRecipe(recipe)end for _,row in ipairs(review.rows)do if row.status==\"new\"and row.selected==true then merged[row.name],imported=cloneRecipe(row.incoming),imported+1 elseif row.status==\"conflict\"and row.selected==true then merged[row.name],imported=cloneRecipe(row.incoming),imported+1 end end local tags={}for name,members in pairs(existingTags or{})do tags[name]=members end for name,members in pairs(incomingTags or{})do tags[name]=members end local valid,err=M.validateRecipes(merged);if not valid then return nil,err end local graphOk,graphErr=M.validateRecipeGraph(merged,tags);if not graphOk then return nil,graphErr end return merged,imported,tags end function M.encodeRecipeFile(recipes,tags)local valid,err=M.validateRecipes(recipes);if not valid then return nil,err end local tagMap,tagsErr=validTags(tags or{});if not tagMap then return nil,tagsErr end local graphOk,graphErr=M.validateRecipeGraph(recipes,tagMap);if not graphOk then return nil,graphErr end local ok,encoded=pcall(textutils.serialise,{schema=1,recipes=recipes,tags=tagMap},{compact=true})if not ok or type(encoded)~=\"string\"then return nil,\"could not serialise recipes\"end return\"return \"..encoded..\"\\n\"end function M.writeRecipeFile(path,recipes,tags)if type(path)~=\"string\"or path==\"\"then return false,\"invalid recipe file path\"end local raw,encodeErr=M.encodeRecipeFile(recipes,tags);if not raw then return false,encodeErr end local parent=fs.getDir(path);if parent~=\"\"and not fs.exists(parent)then fs.makeDir(parent)end local temporary,backup=path..\".new\",path..\".bak\"if fs.exists(temporary)then fs.delete(temporary)end local handle=fs.open(temporary,\"w\");if not handle then return false,\"could not stage recipes\"end handle.write(raw);handle.close()local check=fs.open(temporary,\"r\");local staged=check and check.readAll()or nil;if check then check.close()end if not M.parseRecipeOverlay(staged or\"\")then if fs.exists(temporary)then fs.delete(temporary)end;return false,\"could not validate staged recipes\"end if fs.exists(backup)then fs.delete(backup)end if fs.exists(path)then fs.move(path,backup)end local moved,moveErr=pcall(fs.move,temporary,path)if not moved then if fs.exists(backup)and not fs.exists(path)then fs.move(backup,path)end;if fs.exists(temporary)then fs.delete(temporary)end;return false,tostring(moveErr)end if fs.exists(backup)then fs.delete(backup)end return true end local function selectedRecipe(entry)if type(entry)==\"table\"and type(entry.alternatives)==\"table\"then return entry.alternatives[1]end return entry end local function tagMembers(tags,reference)if reference:sub(1,1)~=\"#\"then return{reference}end local members=tags[reference:sub(2)]if type(members)~=\"table\"or#members==0 then return nil,\"no known members for \"..reference end return members end local function chooseMember(reference,tags,available,recipes)local members,err=tagMembers(tags,reference);if not members then return nil,err end local best for _,member in ipairs(members)do if(available[member]or 0)>0 then return member end if recipes[member]and not best then best=member end end return best,best and nil or(\"no stocked or craftable member for \"..reference)end function M.plan(recipes,stock,item,quantity,options)quantity=tonumber(quantity)if type(item)~=\"string\"or item==\"\"or not quantity or quantity<1 or quantity~=math.floor(quantity)then return nil,\"invalid item or quantity\"end local valid,errorText=M.validateRecipes(recipes)if not valid then return nil,errorText end options=type(options)==\"table\"and options or{}local available,plan,visiting,tags=M.normalise(stock),{},{},type(options.tags)==\"table\"and options.tags or{}local function requireItem(name,needed)local used=options.forceRoot==true and name==item and 0 or math.min(available[name]or 0,needed)available[name]=(available[name]or 0)-used local missing=needed-used if missing<=0 then return true end if visiting[name]then return nil,\"recipe cycle at \"..name end local recipe=selectedRecipe(recipes[name])if not recipe then return nil,\"missing recipe for \"..name..\" (need \"..tostring(missing)..\")\"end if recipe.special or not recipe.output or not recipe.address then return nil,\"recipe \"..name..\" is not supported by Crafting Queue\"end visiting[name]=true local batches=math.ceil(missing/recipe.output)local inputs={}for _,ingredient in ipairs(recipeIngredients(recipe,batches))do local concrete,resolveErr=chooseMember(ingredient.name,tags,available,recipes)if not concrete then return nil,resolveErr end inputs[#inputs+1]={name=concrete,count=ingredient.count,source=ingredient.name,tag=ingredient.name:sub(1,1)==\"#\"and ingredient.name or nil}local ok,err=requireItem(concrete,ingredient.count)if not ok then return nil,err end end visiting[name]=nil local produced=batches*recipe.output available[name]=(available[name]or 0)+produced-missing local dispatch=recipe.address==\"Crafter\"and\"requester\"or\"package\"if dispatch==\"requester\"and type(recipe.grid)~=\"table\"then return nil,\"Crafter recipe \"..name..\" has no 3x3 grid\"end plan[#plan+1]={name=name,batches=batches,produced=produced,requested=missing,address=recipe.address,kind=recipe.kind,dispatch=dispatch,grid=recipe.grid,inputs=inputs,}return true end local ok,err=requireItem(item,quantity)return ok and plan or nil,err end function M.validateExecutionPlan(plan)if type(plan)~=\"table\"or#plan<1 or#plan>96 then return nil,\"invalid remote recipe plan\"end for _,step in ipairs(plan)do if type(step)~=\"table\"or not validReference(step.name)or tostring(step.name):sub(1,1)==\"#\"then return nil,\"invalid remote recipe output\"end if type(step.address)~=\"string\"or step.address==\"\"or#step.address>64 or step.address:find(\"[%c]\")then return nil,\"invalid remote recipe address\"end if step.dispatch~=\"requester\"and step.dispatch~=\"package\"then return nil,\"invalid remote recipe route\"end if(step.address==\"Crafter\")~=(step.dispatch==\"requester\")then return nil,\"invalid remote recipe executor\"end for _,field in ipairs({\"batches\",\"produced\",\"requested\"})do local value=tonumber(step[field])if not value or value<1 or value~=math.floor(value)or value>65536 then return nil,\"invalid remote recipe quantity\"end end if step.dispatch==\"requester\"then if type(step.grid)~=\"table\"then return nil,\"remote Crafter recipe has no grid\"end for slot=1,9 do local value=step.grid[slot]if value~=nil and value~=false and not validReference(value)then return nil,\"invalid remote recipe grid\"end end end if type(step.inputs)~=\"table\"or#step.inputs>64 then return nil,\"invalid remote recipe inputs\"end for _,input in ipairs(step.inputs)do if type(input)~=\"table\"or not validReference(input.name)or tostring(input.name):sub(1,1)==\"#\"then return nil,\"invalid remote recipe input\"end local count=tonumber(input.count)if not count or count<1 or count~=math.floor(count)or count>65536 then return nil,\"invalid remote recipe input quantity\"end if input.source~=nil and not validReference(input.source)then return nil,\"invalid remote recipe input source\"end end end return plan end return M",
  ["ceetos/lib/craft_sources.lua"] = "local M={}local function isType(value,needle)return tostring(value or\"\"):lower():find(needle,1,true)~=nil end function M.kind(value)if type(value)==\"string\"then return{transport=\"local\",name=value}end return type(value)==\"table\"and value or nil end function M.key(value)value=M.kind(value)if not value then return nil end return tostring(value.transport or\"local\")..\":\"..tostring(value.peer or\"\")..\":\"..tostring(value.name or\"\")end function M.label(value)value=M.kind(value)if not value then return\"Not selected\"end if value.transport==\"peer\"then return\"Peer \"..tostring(value.peer)..\" / \"..tostring(value.name)end return tostring(value.name)end function M.discoverLocal()local tickers,requesters={},{}for _,name in ipairs(peripheral.getNames())do local typeName=peripheral.getType(name)if isType(typeName,\"stockticker\")then tickers[#tickers+1]={transport=\"local\",name=name,type=typeName,label=\"Local / \"..name}elseif isType(typeName,\"redstonerequester\")then requesters[#requesters+1]={transport=\"local\",name=name,type=typeName,label=\"Local / \"..name}end end table.sort(tickers,function(a,b)return a.name<b.name end)table.sort(requesters,function(a,b)return a.name<b.name end)return tickers,requesters end function M.matches(value,kind)value=M.kind(value)if not value or type(value.name)~=\"string\"then return false end local needle=(kind==\"ticker\"or kind==\"stockticker\")and\"stockticker\"or\"redstonerequester\"return isType(value.type,needle)end return M",
  ["ceetos/lib/program_capabilities.lua"] = "local store=require(\"ceetos.lib.store\")local auth=require(\"ceetos.lib.auth\")local audit=require(\"ceetos.lib.audit\")local M={}local PATH=\"/ceetos/data/program-capabilities.lua\"local SCHEMA=1 local SCRATCH_ROOT=\"/ceetos/data/programs\"local PROTECTED_ROOTS={\"/ceetos\",\"/ceetos-dev\",\"/rom\",}local READ_ONLY_METHODS={getAngles=true,getSpeed=true,getStress=true,getStressCapacity=true,getTargetSpeed=true,getItemDetail=true,getInventoryName=true,getMetadata=true,getFluidDetail=true,getEnergy=true,getCursorPos=true,getSize=true,getTextScale=true,isRunning=true,isPresent=true,list=true,size=true,}local function now()return os.epoch(\"utc\")end local function copy(value,seen)if type(value)~=\"table\"then return value end seen=seen or{}if seen[value]then return seen[value]end local result={}seen[value]=result for key,item in pairs(value)do result[copy(key,seen)]=copy(item,seen)end return result end local function defaultConfig()return{schema=SCHEMA,programs={}}end local function normalise(path)if type(path)~=\"string\"or path==\"\"or#path>240 or path:find(\"[%z\\1-\\31]\")then return nil,\"invalid program path\"end local parts={}for part in path:gmatch(\"[^/]+\")do if part==\"..\"then if#parts>0 then table.remove(parts)end elseif part~=\".\"and part~=\"\"then parts[#parts+1]=part end end return\"/\"..table.concat(parts,\"/\")end local function readSource(path)local handle,err=fs.open(path,\"r\")if not handle then return nil,err or\"could not read program\"end local source=handle.readAll()handle.close()return source end local function legacyHash(source)local hash=2166136261 for index=1,#source do hash=bit32.bxor(hash,source:byte(index))hash=(hash*16777619)%4294967296 end return string.format(\"fnv1a32:%08x\",hash)end local function programId(path)return\"p-\"..legacyHash(path):match(\":(%x+)$\")end local function loadConfig()local config=store.readFresh(PATH,defaultConfig())if type(config)~=\"table\"then return defaultConfig()end if type(config.programs)~=\"table\"then config.programs={}end config.schema=SCHEMA return config end local function saveConfig(config)local ok,err=store.writeAtomic(PATH,config)assert(ok,\"could not save program capabilities: \"..tostring(err))end local function adminActor()if auth.refreshSession then auth.refreshSession()end auth.require(\"admin\")return auth.current and auth.current.name or\"admin\"end local function pathWithin(path,root)return path==root or path:sub(1,#root+1)==root..\"/\"end local function protectedProgramPath(path)if path==\"/startup.lua\"then return true end for _,root in ipairs(PROTECTED_ROOTS)do if pathWithin(path,root)then return true end end return false end local function normaliseProgramPath(path)local normalized,err=normalise(path)if not normalized then return nil,err end if protectedProgramPath(normalized)then return nil,\"protected CeetOS/CraftOS program path\"end if normalized:sub(-4):lower()~=\".lua\"then return nil,\"program must be a Lua file\"end if not fs.exists(normalized)or fs.isDir(normalized)then return nil,\"program file not found\"end return normalized end local function defaultRecord(path,source,owner)return{path=path,id=programId(path),sourceHash=legacyHash(source),hashKind=\"fnv1a32-legacy-integrity-marker\",owner=owner or\"unassigned\",trust=\"sandboxed\",capabilities={scratch=true,peripherals={}},createdAt=now(),updatedAt=now(),}end local function cleanMethods(methods)if type(methods)~=\"table\"then return nil,\"methods must be a list\"end local seen,result={},{}for _,method in ipairs(methods)do if type(method)~=\"string\"or not READ_ONLY_METHODS[method]then return nil,\"method is not an approved read-only capability\"end if not seen[method]then seen[method]=true;result[#result+1]=method end end if#result==0 then return nil,\"at least one read-only method is required\"end table.sort(result)return result end local function recordIdAvailable(config,id,path)for savedPath,saved in pairs(config.programs)do if savedPath~=path and type(saved)==\"table\"and saved.id==id then return false end end return true end local function recordFrom(config,path,source)local saved=config.programs[path]local currentHash=legacyHash(source)local id=programId(path)if not recordIdAvailable(config,id,path)then return nil,nil,\"program capability ID collision; choose a different program path\"end if type(saved)==\"table\"and saved.sourceHash==currentHash and saved.path==path then local record=copy(saved)record.id=id record.hashKind=\"fnv1a32-legacy-integrity-marker\"record.capabilities=type(record.capabilities)==\"table\"and record.capabilities or{}record.capabilities.scratch=true record.capabilities.peripherals=type(record.capabilities.peripherals)==\"table\"and record.capabilities.peripherals or{}return record,false,nil end local record=defaultRecord(path,source,type(saved)==\"table\"and saved.owner or nil)record.stale=type(saved)==\"table\"return record,record.stale,nil end local function scratchPath(record)return SCRATCH_ROOT..\"/\"..record.id end local function normaliseScratch(path,root)if type(path)~=\"string\"or#path>240 or path:find(\"[%z\\1-\\31]\")then return nil end local candidate=path:sub(1,1)==\"/\"and path or root..\"/\"..path local normalized=normalise(candidate)if not normalized or not pathWithin(normalized,root)then return nil end return normalized end local function safeTerm()local source=term return{write=source.write,blit=source.blit,clear=source.clear,clearLine=source.clearLine,scroll=source.scroll,getSize=source.getSize,getCursorPos=source.getCursorPos,setCursorPos=source.setCursorPos,getCursorBlink=source.getCursorBlink,setCursorBlink=source.setCursorBlink,setTextColor=source.setTextColor,setTextColour=source.setTextColour,getTextColor=source.getTextColor,getTextColour=source.getTextColour,setBackgroundColor=source.setBackgroundColor,setBackgroundColour=source.setBackgroundColour,getBackgroundColor=source.getBackgroundColor,getBackgroundColour=source.getBackgroundColour,isColor=source.isColor,isColour=source.isColour,}end local function safeTextutils()return{serialise=textutils.serialise,serialize=textutils.serialize,serialiseJSON=textutils.serialiseJSON,serializeJSON=textutils.serializeJSON,unserialiseJSON=textutils.unserialiseJSON,unserializeJSON=textutils.unserializeJSON,formatTime=textutils.formatTime,urlEncode=textutils.urlEncode,}end local function tableCopy(value)local output={}for key,item in pairs(value or{})do output[key]=item end return output end local function safeFilesystem(record,actor,deny)local root=scratchPath(record)if not fs.exists(root)then local made,makeErr=pcall(fs.makeDir,root)if not made or makeErr==false then error(\"could not initialise program scratch storage\",2)end end local function target(path)local value=normaliseScratch(path,root)if not value then deny(\"filesystem path\")end return value end local function mutable(path)local value=target(path)if value==root then deny(\"program scratch root\")end return value end local proxy={}function proxy.exists(path)return fs.exists(target(path))end function proxy.isDir(path)return fs.isDir(target(path))end function proxy.list(path)return fs.list(target(path))end function proxy.getSize(path)return fs.getSize(target(path))end function proxy.open(path,mode)mode=tostring(mode or\"r\")if mode~=\"r\"and mode~=\"rb\"and mode~=\"w\"and mode~=\"wb\"and mode~=\"a\"and mode~=\"ab\"then deny(\"filesystem mode\")end return fs.open((mode==\"r\"or mode==\"rb\")and target(path)or mutable(path),mode)end function proxy.makeDir(path)return fs.makeDir(mutable(path))end function proxy.delete(path)return fs.delete(mutable(path))end function proxy.move(from,to)return fs.move(mutable(from),mutable(to))end function proxy.copy(from,to)return fs.copy(mutable(from),mutable(to))end function proxy.combine(a,b)local value=normaliseScratch(tostring(a or\"\")..\"/\"..tostring(b or\"\"),root)if not value then deny(\"filesystem path\")end return value end function proxy.getDir(path)return fs.getDir(target(path))end function proxy.getName(path)return fs.getName(target(path))end return proxy end local function safePeripheral(record,deny)local grants=record.trust==\"trusted\"and record.capabilities and record.capabilities.peripherals or{}local function methodsFor(name)local grant=type(grants)==\"table\"and grants[name]or nil local allowed={}for _,method in ipairs(type(grant)==\"table\"and grant.methods or{})do if READ_ONLY_METHODS[method]then allowed[method]=true end end return allowed end local proxy={}function proxy.getNames()local result={}for name in pairs(grants or{})do local methods=methodsFor(name)if next(methods)and peripheral.isPresent(name)then result[#result+1]=name end end table.sort(result)return result end function proxy.isPresent(name)return next(methodsFor(name))~=nil and peripheral.isPresent(name)or false end function proxy.getType(name)if next(methodsFor(name))==nil then deny(\"peripheral \"..tostring(name))end return peripheral.getType(name)end function proxy.getMethods(name)local result={}for method in pairs(methodsFor(name))do result[#result+1]=method end if#result==0 then deny(\"peripheral \"..tostring(name))end table.sort(result)return result end function proxy.wrap(name)local allowed=methodsFor(name)if next(allowed)==nil then deny(\"peripheral \"..tostring(name))end local wrapped={}for method in pairs(allowed)do wrapped[method]=function(...)local raw=peripheral.wrap(name)if not raw or type(raw[method])~=\"function\"then error(\"granted peripheral is unavailable\",2)end local args={...}if args[1]==wrapped then table.remove(args,1)end return raw[method](table.unpack(args))end end return wrapped end return proxy end function M.normaliseProgramPath(path)return normaliseProgramPath(path)end function M.legacySourceHash(source)assert(type(source)==\"string\",\"source must be a string\")return legacyHash(source)end function M.isReadOnlyMethod(method)return READ_ONLY_METHODS[method]==true end function M.describe(path)local normalized,err=normaliseProgramPath(path)if not normalized then return nil,err end local source,readErr=readSource(normalized)if not source then return nil,readErr end local record,_,recordErr=recordFrom(loadConfig(),normalized,source)if not record then return nil,recordErr end return copy(record)end function M.create(path,owner)local actor=adminActor()local normalized,err=normaliseProgramPath(path)assert(normalized,err)if owner~=nil then assert(type(owner)==\"string\"and owner:match(\"^[%w_.%-]+$\")and#owner<=64,\"invalid program owner\")end local source=assert(readSource(normalized))local config=loadConfig()assert(recordIdAvailable(config,programId(normalized),normalized),\"program capability ID collision; choose a different program path\")local record=defaultRecord(normalized,source,owner or actor)config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.capability_create\",{path=normalized,owner=record.owner})return copy(record)end function M.setTrust(path,trust)local actor=adminActor()assert(trust==\"sandboxed\"or trust==\"trusted\",\"invalid program trust state\")local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.trust,record.updatedAt=trust,now()if trust~=\"trusted\"then record.capabilities.peripherals={}end config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.trust\",{path=normalized,trust=trust})return copy(record)end function M.setOwner(path,owner)local actor=adminActor()assert(type(owner)==\"string\"and owner:match(\"^[%w_.%-]+$\")and#owner<=64,\"invalid program owner\")local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.owner,record.updatedAt=owner,now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.owner\",{path=normalized,owner=owner})return copy(record)end function M.grantReadOnlyPeripheral(path,name,methods)local actor=adminActor()assert(type(name)==\"string\"and name~=\"\"and#name<=128 and not name:find(\"[%z\\1-\\31]\"),\"invalid peripheral name\")local clean,methodErr=cleanMethods(methods)assert(clean,methodErr)local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)assert(record.trust==\"trusted\",\"admin must trust the program before granting peripherals\")record.capabilities.peripherals[name]={methods=clean}record.updatedAt=now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.peripheral_grant\",{path=normalized,peripheral=name,methods=clean})return copy(record)end function M.revokePeripheral(path,name)local actor=adminActor()local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.capabilities.peripherals[name]=nil record.updatedAt=now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.peripheral_revoke\",{path=normalized,peripheral=tostring(name)})return copy(record)end function M.viewerEnvironment(path,actor)local record,err=M.describe(path)if not record then return nil,err end actor=actor or\"viewer\"local denied={}local function deny(action)if not denied[action]then denied[action]=true audit.log(actor,\"program.denied\",{path=record.path,action=action})end error(\"permission denied: \"..action,3)end local function pullEvent(filter)while true do local event={os.pullEventRaw(filter)}if event[1]~=\"terminate\"then return table.unpack(event)end end end local safeOs={pullEvent=pullEvent,startTimer=os.startTimer,cancelTimer=os.cancelTimer,clock=os.clock,time=os.time,day=os.day,epoch=os.epoch,getComputerID=os.getComputerID,getComputerLabel=os.getComputerLabel,}local environment={_VERSION=_VERSION,assert=assert,error=error,ipairs=ipairs,next=next,pairs=pairs,pcall=pcall,select=select,tonumber=tonumber,tostring=tostring,type=type,xpcall=xpcall,print=print,printError=printError,read=read,write=write,math=tableCopy(math),string=tableCopy(string),table=tableCopy(table),coroutine=tableCopy(coroutine),bit32=tableCopy(bit32),utf8=tableCopy(utf8),keys=tableCopy(keys),colors=tableCopy(colors),colours=tableCopy(colours or colors),term=safeTerm(),textutils=safeTextutils(),os=safeOs,fs=safeFilesystem(record,actor,deny),peripheral=safePeripheral(record,deny),}return setmetatable(environment,{__index=function()return nil end}),record end function M.loadViewer(path,actor)local environment,record=M.viewerEnvironment(path,actor)if not environment then return nil,record end local chunk,err=loadfile(record.path,nil,environment)if not chunk then audit.log(actor or\"viewer\",\"program.denied\",{path=record.path,action=\"load\"})return nil,err end return chunk,record end function M.scratchPath(path)local record,err=M.describe(path)if not record then return nil,err end return scratchPath(record)end M.PATH=PATH M.SCHEMA=SCHEMA M.HASH_KIND=\"fnv1a32-legacy-integrity-marker\"return M",
  ["ceetos/bin/ceetver.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local ok,version=pcall(require,\"ceetos.lib.version\")print(\"CeetOS \"..(ok and version.string or\"0.12.0\"))",
  ["ceetos/bin/ceetdevices.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local peripherals=require(\"ceetos.lib.peripherals\")local shared={}for _,item in ipairs(peripherals.describe())do shared[item.name]=item.mode end for _,item in ipairs(peripherals.listLocal())do print(item.name..\"\\t\"..tostring(item.type or\"unknown\")..\"\\t\"..(shared[item.name]or\"not shared\"))end",
  ["ceetos/bin/ceetcraft.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth,store,crafting=require(\"ceetos.lib.auth\"),require(\"ceetos.lib.store\"),require(\"ceetos.lib.crafting\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local canMutate=auth.allowed(\"operator\")local STATE,COMMANDS=\"/ceetos/data/crafting-state.lua\",\"/ceetos/data/crafting-commands\"local query,filter,focus,selectedStock,selectedCart,buttonIndex,actionPage=\"\",\"all\",\"stock\",1,1,1,1 local RECIPE_FILE,RECIPE_IMPORTS=\"/ceetos/data/recipes.lua\",\"/ceetos/data/recipe-imports.lua\"local modal,pending,serial,armedAction,lastRowClick,feedback=nil,{},0,nil,nil,nil local importReview,importScroll,importArmed=nil,0,nil local actions={{label=\"Add\",action=\"add\"},{label=\"Edit\",action=\"edit\"},{label=\"Del\",action=\"delete\"},{label=\"Craft\",action=\"mode\"},{label=\"Ord D\",action=\"destination\"},{label=\"Sources\",action=\"sources\"},{label=\"Find\",action=\"search\"},{label=\"Filt\",action=\"filter\"},{label=\"R\",action=\"refresh\"},{label=\"Send O\",action=\"submit\"},{label=\"Retry\",action=\"retry\"},{label=\"Can C\",action=\"cancel\"},{label=\"Back\",action=\"back\"},}local function state()store.invalidate(STATE)return store.read(STATE,{stock={},cart={},queue={},retries={},acknowledgements={},config={orderAddress=\"Order\"},history={}})end local function clipped(value,width)return terminalUi.clip(value,math.max(1,tonumber(width)or 1),\"~\")end local function queueCommand(action,data,label)feedback=nil serial=serial+1 local token=\"craft-ui-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(serial)if not fs.exists(COMMANDS)then fs.makeDir(COMMANDS)end data=data or{};data.action,data.token=action,token local handle=fs.open(fs.combine(COMMANDS,token..\".lua\"),\"w\")if not handle then return false,\"could not queue \"..tostring(label or action)end handle.write(textutils.serialise(data));handle.close()pending[token]={label=label or action,at=os.epoch(\"utc\")}return true end local function cleanPending(s)for token,row in pairs(pending)do local acknowledgement=(s.acknowledgements or{})[token]if acknowledgement then pending[token]=nil if acknowledgement.ok~=true then feedback=\"Cannot \"..row.label..\": \"..tostring(acknowledgement.error or\"request was rejected\")end elseif os.epoch(\"utc\")-row.at>5000 then pending[token]=nil;feedback=\"Crafting service did not acknowledge \"..row.label end end end local function pendingText()for _,row in pairs(pending)do return\"Pending: \"..row.label end return feedback end local function startModal(kind,label,value,replaceOnInput)modal={kind=kind,label=label,value=tostring(value or\"\"),replaceOnInput=replaceOnInput==true}end local function recordRecipeImport(entry)local history=store.read(RECIPE_IMPORTS,{})history[#history+1]=entry while#history>16 do table.remove(history,1)end store.write(RECIPE_IMPORTS,history)end local function currentOverlay(s)local handle=fs.exists(RECIPE_FILE)and fs.open(RECIPE_FILE,\"r\")or nil local source=handle and handle.readAll()or nil if handle then handle.close()end if source then local overlay,err=crafting.parseRecipeOverlay(source)if overlay then return overlay end return nil,err end return{schema=1,recipes={},tags={}}end local function beginRecipeImport(event,s)if not canMutate then feedback=\"Recipe import requires operator access\";return end local transfers=event and event.getFiles and event.getFiles()or nil if type(transfers)~=\"table\"or#transfers~=1 then feedback=\"Drop exactly one recipe pack into Crafting Queue\";return end local transfer,chunks,total=transfers[1],{},0 if not transfer or type(transfer.read)~=\"function\"then feedback=\"Dropped file is not readable\";return end while true do local chunk=transfer.read(4096)if not chunk then break end if type(chunk)~=\"string\"then feedback=\"Dropped recipe pack has invalid data\";return end total=total+#chunk if total>crafting.RECIPE_PACK_MAX_BYTES then feedback=\"Recipe pack exceeds the 64 KiB import limit\";return end chunks[#chunks+1]=chunk end local pack,packErr=crafting.parseRecipePack(table.concat(chunks))if not pack then feedback=\"Recipe pack rejected: \"..tostring(packErr);recordRecipeImport({at=os.epoch(\"utc\"),source=transfer.getName and transfer.getName()or\"dropped file\",status=\"rejected\",error=tostring(packErr):sub(1,120)});return end local overlay,overlayErr=currentOverlay(s)if not overlay then feedback=\"Could not read local recipe overlay: \"..tostring(overlayErr);return end local review,reviewErr=crafting.reviewRecipePack(s.recipes or{},pack.recipes)if not review then feedback=\"Recipe pack rejected: \"..tostring(reviewErr);return end importReview={source=tostring(transfer.getName and transfer.getName()or\"recipe pack\"):sub(1,96),overlay=overlay,incomingTags=pack.tags or{},review=review,selected=1}importScroll,importArmed,feedback=0,nil,nil end local function importSelection(row)if row and(row.status==\"new\"or row.status==\"conflict\")then row.selected=not row.selected end end local function applyImportAction(action)if not importReview then return end local rows=importReview.review.rows if action==\"all_new\"then for _,row in ipairs(rows)do if row.status==\"new\"then row.selected=true end end elseif action==\"keep_conflicts\"then for _,row in ipairs(rows)do if row.status==\"conflict\"then row.selected=false end end elseif action==\"replace_conflicts\"then for _,row in ipairs(rows)do if row.status==\"conflict\"then row.selected=true end end elseif action==\"cancel_import\"then recordRecipeImport({at=os.epoch(\"utc\"),source=importReview.source,status=\"cancelled\",counts=importReview.review.counts})importReview=nil elseif action==\"apply_import\"then local merged,importedOrErr,tags=crafting.mergeRecipePack(importReview.overlay.recipes,importReview.review,importReview.overlay.tags,importReview.incomingTags)if not merged then feedback=\"Recipe import rejected: \"..tostring(importedOrErr);return end local saved,saveErr=crafting.writeRecipeFile(RECIPE_FILE,merged,tags)if not saved then feedback=\"Could not save recipes: \"..tostring(saveErr);return end recordRecipeImport({at=os.epoch(\"utc\"),source=importReview.source,status=\"imported\",imported=importedOrErr,counts=importReview.review.counts})importReview,feedback=nil,nil end end local function finishModal(s,rows)local kind,value=modal.kind,modal.value;modal=nil if kind==\"search\"then query=value elseif kind==\"destination\"and value~=\"\"then queueCommand(\"destination\",{value=value},\"Order address\")elseif kind==\"add\"then local n=tonumber(value);if n and n>0 and rows[selectedStock]then queueCommand(\"add\",{name=rows[selectedStock].name,count=n},\"Add\")end elseif kind==\"edit\"then local n=tonumber(value);if n and n>0 and s.cart[selectedCart]then queueCommand(\"edit\",{index=selectedCart,count=n},\"Edit\")end elseif kind==\"cancel_confirm\"then queueCommand(\"cancel_active\",{},\"Cancel craft\")end end local function sourceChoices(s,target)local choices={}if target==\"requester\"then choices[#choices+1]={label=\"No Redstone Requester\",value=false}end local key=target==\"recipes\"and\"recipeServers\"or(target..\"s\")for _,source in ipairs(((s.sources or{})[key]or{}))do choices[#choices+1]={label=tostring(source.label or((source.transport==\"peer\"and\"Peer \"..tostring(source.peer)..\" / \")or\"Local / \")..tostring(source.name)),value=source}end return choices end local function chooseSource(s,target)local choices,selected=sourceChoices(s,target),1 if#choices==0 then return end while true do local w,h=term.getSize()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()term.setBackgroundColor(colors.blue);term.setCursorPos(1,1);term.clearLine();term.setCursorPos(2,1);term.write(\"CeetOS Crafting Queue | Select \"..target)term.setTextColor(colors.lightGray);term.setCursorPos(2,3);term.write(\"Use arrows/Enter or click a source. Backspace cancels.\")local targets={}for index,choice in ipairs(choices)do local y=4+index-1;if y>=h then break end term.setBackgroundColor(index==selected and colors.gray or colors.black);term.setTextColor(index==selected and colors.white or colors.lightGray);term.setCursorPos(2,y);term.clearLine();term.write(choice.label:sub(1,w-3))targets[#targets+1]={y=y,index=index}end local event,a,b,c=os.pullEvent()if event==\"key\"then local key=keys.getName(a)if key==\"backspace\"or key==\"q\"then return elseif key==\"up\"then selected=math.max(1,selected-1)elseif key==\"down\"then selected=math.min(#choices,selected+1)elseif key==\"enter\"then queueCommand(\"source\",{target=target,value=choices[selected].value},target..\" source\");return end elseif event==\"mouse_click\"and a==1 then for _,hit in ipairs(targets)do if c==hit.y then selected=hit.index;break end end elseif event==\"mouse_up\"and a==1 then for _,hit in ipairs(targets)do if c==hit.y then queueCommand(\"source\",{target=target,value=choices[hit.index].value},target..\" source\");return end end end end end local function applyAction(action,s,rows)if action==\"back\"then return\"exit\"end if action==\"more\"then actionPage=actionPage+1;buttonIndex=1;return end if not canMutate and action~=\"search\"and action~=\"filter\"and action~=\"refresh\"then return end if action==\"search\"then startModal(\"search\",\"Find ticker items\",query)elseif action==\"filter\"then filter=filter==\"all\"and\"craftable\"or(filter==\"craftable\"and\"stocked\"or\"all\")elseif action==\"refresh\"then queueCommand(\"refresh\",{},\"Refresh\")elseif action==\"mode\"then if(s.config or{}).requester then queueCommand(\"mode\",{value=not s.craftMode},\"Craft mode\")end elseif action==\"destination\"then startModal(\"destination\",\"Order address\",(s.config or{}).orderAddress or\"Order\")elseif action==\"sources\"then chooseSource(s,\"ticker\");s=state();chooseSource(s,\"requester\");s=state();chooseSource(s,\"recipes\")elseif action==\"submit\"then queueCommand(\"submit\",{},\"Submit\")elseif action==\"retry\"and#(s.retries or{})>0 then queueCommand(\"retry\",{index=1},\"Retry\")elseif action==\"cancel\"and s.active then startModal(\"cancel_confirm\",\"Cancel active craft? Issued Create packages may still arrive\",\"\")elseif action==\"delete\"and focus==\"cart\"then queueCommand(\"remove\",{index=selectedCart},\"Remove\")elseif action==\"add\"and focus==\"stock\"and rows[selectedStock]then startModal(\"add\",\"Add \"..rows[selectedStock].name..\" quantity\",\"1\",true)elseif action==\"edit\"and focus==\"cart\"and s.cart[selectedCart]then startModal(\"edit\",\"Set \"..s.cart[selectedCart].name..\" quantity\",s.cart[selectedCart].count,true)end end local function frame(x1,y1,x2,y2,title)return terminalUi.box(x1,y1,x2,y2,title,colors.gray,colors.lightBlue)end local function drawButton(x,y,entry,selected,armed,targets)local text=\"[\"..entry.label..\"]\"local width=select(1,term.getSize())if x+#text-1>width then return x end local background=armed and colors.yellow or(selected and colors.gray or colors.black)local enabled=canMutate or entry.action==\"search\"or entry.action==\"filter\"or entry.action==\"refresh\"or entry.action==\"back\"terminalUi.write(x,y,text,armed and colors.black or(enabled and(selected and colors.white or colors.lightGray)or colors.gray),background)targets[#targets+1]={x1=x,x2=x+#text-1,y1=y,y2=y,action=entry.action}return x+#text+1 end local function visibleActions(w)local pageSize=w>=48 and 7 or 5 local pages=math.max(1,math.ceil(#actions/pageSize))actionPage=math.max(1,math.min(pages,actionPage))local out,first,last={},(actionPage-1)*pageSize+1,math.min(#actions,actionPage*pageSize)for index=first,last do out[#out+1]=actions[index]end if pages>1 then out[#out+1]={label=\"More \"..actionPage..\"/\"..pages,action=\"more\"}end buttonIndex=math.max(1,math.min(#out,buttonIndex))return out end local function drawActionRows(w,h,targets)local entries=visibleActions(w)local firstCount=math.ceil(#entries/2)terminalUi.clearLine(h-2,colors.black);terminalUi.clearLine(h-1,colors.black)local x,row=2,h-2 for index,entry in ipairs(entries)do if index==firstCount+1 then x,row=2,h-1 end x=drawButton(x,row,entry,focus==\"actions\"and buttonIndex==index,armedAction==entry.action,targets)end return entries end local function drawModal(w,h)if not modal then return end terminalUi.clearLine(h-2,colors.gray);terminalUi.write(2,h-2,modal.label,colors.white,colors.gray,w-3)if modal.kind==\"cancel_confirm\"then terminalUi.clearLine(h-1,colors.gray);terminalUi.write(2,h-1,\"[Confirm cancel] [Keep running]\",colors.white,colors.gray,w-3)else terminalUi.clearLine(h-1,colors.gray)if modal.replaceOnInput then terminalUi.write(2,h-1,modal.value,colors.black,colors.lightBlue,w-4)else terminalUi.write(2,h-1,\"> \"..modal.value,colors.white,colors.gray,w-4)end term.setCursorPos(math.min(w-1,4+#modal.value),h-1)terminalUi.clearLine(h,colors.gray);terminalUi.write(2,h,\"[Save] [Cancel]\",colors.white,colors.gray,w-3)end end local function drawImportReview()local w,h=term.getSize()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if w<36 or h<15 then terminalUi.minimum(\"Recipe import\",\"Recipe review needs 36x15\",\"Resize | Backspace\")return{targets={},rows={}}end local review,counts=importReview.review,importReview.review.counts terminalUi.clearLine(1,colors.blue);terminalUi.write(2,1,\"CeetOS Crafting Queue | Review recipe pack\",colors.white,colors.blue,w-2)terminalUi.write(2,3,\"File: \"..clipped(importReview.source,w-8),colors.lightGray,colors.black,w-3)terminalUi.write(2,4,(\"New: %d  Conflicts: %d  Unchanged: %d  Invalid: %d\"):format(counts.new or 0,counts.conflict or 0,counts.unchanged or 0,counts.invalid or 0),colors.white,colors.black,w-3)frame(1,5,w,h-4,\"Recipes\")local capacity=math.max(1,h-10)importReview.selected=math.max(1,math.min(math.max(1,#review.rows),importReview.selected or 1))if importReview.selected<=importScroll then importScroll=importReview.selected-1 end if importReview.selected>importScroll+capacity then importScroll=importReview.selected-capacity end importScroll=math.max(0,math.min(math.max(0,#review.rows-capacity),importScroll))local targets,visible={},{}for offset=1,capacity do local index,row,y=importScroll+offset,review.rows[importScroll+offset],5+offset if row then local marker=row.status==\"new\"and(row.selected and\"[+]\"or\"[ ]\")or row.status==\"conflict\"and(row.selected and\"[Replace]\"or\"[Keep]\")or\"[Same]\"local colour=row.status==\"new\"and colors.lime or row.status==\"conflict\"and colors.yellow or colors.gray terminalUi.write(3,y,marker..\" \"..row.name,colour,index==importReview.selected and colors.gray or colors.black,w-5)targets[#targets+1]={x1=2,x2=w-1,y1=y,y2=y,action=\"select_recipe\",index=index}visible[#visible+1]=row end end local selected=review.rows[importReview.selected]if selected then local localText=selected.existing and(\"Local: \"..tostring(selected.existing.output)..\" via \"..tostring(selected.existing.address))or\"Local: none\"local incomingText=\"Incoming: \"..tostring(selected.incoming.output)..\" via \"..tostring(selected.incoming.address)terminalUi.write(2,h-3,clipped(localText..\" | \"..incomingText,w-3),colors.lightGray,colors.black,w-3)end local buttons={{label=\"Import\",action=\"apply_import\"},{label=\"All new\",action=\"all_new\"},{label=\"Keep conflicts\",action=\"keep_conflicts\"},{label=\"Replace conflicts\",action=\"replace_conflicts\"},{label=\"Cancel\",action=\"cancel_import\"},}local x,buttonRow=2,h-2 for _,button in ipairs(buttons)do local text=\"[\"..button.label..\"]\"if x+#text-1>w-1 and buttonRow==h-2 then x,buttonRow=2,h-1 end if x+#text-1<=w-1 then terminalUi.write(x,buttonRow,text,importArmed==button.action and colors.black or colors.white,importArmed==button.action and colors.yellow or colors.gray)targets[#targets+1]={x1=x,x2=x+#text-1,y1=buttonRow,y2=buttonRow,action=button.action}x=x+#text+1 end end terminalUi.write(2,h,\"Arrows select | Space toggle | Enter import | A/K/X | Backspace cancel\",colors.gray,colors.black,w-3)return{targets=targets,rows=visible}end local function draw(s,rows)local w,h=term.getSize();term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if w<36 or h<15 then return{targets=terminalUi.minimum(\"CeetOS Crafting Queue\",\"Crafting Queue needs 36x15\",\"Resize | Backspace\")or{},compact=true,actions={}}end local config=s.config or{}local tickerLabel=config.ticker and tostring(config.ticker.label or config.ticker.name)or\"not selected\"local requesterLabel=config.requester and tostring(config.requester.label or config.requester.name)or\"none\"local recipeLabel=config.recipeServer and tostring(config.recipeServer.label or(\"Peer \"..tostring(config.recipeServer.peer)))or\"local\"terminalUi.clearLine(1,colors.blue);terminalUi.write(2,1,\"CeetOS Crafting Queue | Ticker: \"..tickerLabel..\" | Recipes: \"..recipeLabel,colors.white,colors.blue,w-2)local items,total=crafting.cartTotals(s.cart)frame(1,2,w,5,\"Order\")terminalUi.write(3,3,\"Destination: \"..tostring(config.orderAddress or\"Order\")..\"   Craft mode: \"..(s.craftMode and\"ON\"or\"OFF\")..(config.requester and\"\"or\" (requires requester)\"),colors.white,colors.black,w-5)terminalUi.write(3,4,(\"Cart: %d items / %d units    Filter: %s    Find: %s\"):format(items,total,filter,query==\"\"and\"all\"or query),colors.lightGray,colors.black,w-5)local active,status=s.active,pendingText()or\"Ready\"if active then local item=active.items and active.items[active.index]if active.phase==\"await_crafted_output\"then local route=active.stepRoute or\"recipe route\"status=(\"%s; waiting for +%s %s in Stock Ticker\"):format(route,tostring(active.expectedOutput or\"?\"),tostring(active.waitingFor or item and item.name or\"recipe\"))elseif active.phase==\"craft\"and active.pendingCraftRequests then status=(\"Requester batching %s; %s craft request%s remaining\"):format(tostring(item and item.name or\"recipe\"),tostring(active.pendingCraftRequests),active.pendingCraftRequests==1 and\"\"or\"s\")elseif active.phase==\"await_package_confirmation\"or active.phase==\"await_peer_package\"then status=(\"Stock Ticker -> %s; package %s/%s: waiting for %s\"):format(tostring(active.stepAddress or\"destination\"),tostring(active.packageIndex or 1),tostring(active.packageCount or\"?\"),tostring(active.waitingFor or\"ingredient\"))elseif active.phase==\"await_delivery_confirmation\"then status=(\"Order submitted to %s; waiting for ticker decrease of %s\"):format(tostring(active.destination),tostring(active.accepted or\"?\"))else status=(\"%s | %s  requested:%s accepted:%s dispatched:%s remaining:%s\"):format(tostring(active.phase),tostring(item and item.name or\"finishing\"),tostring(active.requested or(item and item.count)or 0),tostring(active.accepted or\"-\"),tostring(active.delivered or 0),tostring(active.remaining or\"-\"))end end frame(1,6,w,8,active and\"Active job\"or\"Status\")terminalUi.write(3,7,s.error or status,(s.error or feedback)and colors.red or(active and colors.yellow or colors.lightGray),colors.black,w-5)local split,top,bottom=math.max(24,math.floor(w*.57)),10,h-4 frame(1,9,split,bottom,\"Items & recipes\")frame(split+1,9,w,bottom,\"Request list\")local lines=math.max(0,bottom-top)for i=1,lines do local y,item,cart=top+i-1,rows[i],(s.cart or{})[i]if item then terminalUi.write(3,y,crafting.displayRow(item.name,item.count,split-4),item.craftable and colors.lime or colors.white,focus==\"stock\"and selectedStock==i and colors.gray or colors.black,split-4)end if cart then terminalUi.write(split+3,y,crafting.displayRow(cart.name,cart.count,w-split-4),colors.white,focus==\"cart\"and selectedCart==i and colors.gray or colors.black,w-split-4)end end local history,retry=(s.history or{})[#(s.history or{})],(s.retries or{})[1]local detail=s.recoveryNotice or(history and(\"Last: \"..tostring(history.status)..\" \"..tostring(history.name or\"job\")..\" \"..tostring(history.dispatched or history.delivered or 0)..\"/\"..tostring(history.requested or 0)..(history.error and(\" - \"..tostring(history.error))or\"\"))or\"No dispatched orders yet\")if retry then detail=detail..\" | Retry: \"..tostring(retry.items[1].name)..\" x\"..tostring(retry.items[1].count)end terminalUi.write(2,bottom+1,detail,colors.gray,colors.black,w-2)local targets={};local actionEntries=drawActionRows(w,h,targets);if modal then drawModal(w,h)end return{split=split,top=top,bottom=bottom-1,targets=targets,actions=actionEntries}end local function hit(targets,x,y)return terminalUi.hit(targets,x,y)end local timer=os.startTimer(0.2)while true do local s=state();cleanPending(s)local rows=crafting.rows(s.stock,query,filter,s.recipes)selectedStock=math.max(1,math.min(math.max(1,#rows),selectedStock));selectedCart=math.max(1,math.min(math.max(1,#s.cart),selectedCart))local layout=importReview and drawImportReview()or draw(s,rows)local event,a,b,c=os.pullEvent()if event==\"timer\"and a==timer then timer=os.startTimer(0.2)elseif importReview then if event==\"key\"then local key=keys.getName(a)if key==\"backspace\"or key==\"q\"then applyImportAction(\"cancel_import\")elseif key==\"up\"then importReview.selected=math.max(1,(importReview.selected or 1)-1)elseif key==\"down\"then importReview.selected=math.min(#importReview.review.rows,(importReview.selected or 1)+1)elseif key==\"space\"or key==\"left\"or key==\"right\"then importSelection(importReview.review.rows[importReview.selected])elseif key==\"a\"then applyImportAction(\"all_new\")elseif key==\"k\"then applyImportAction(\"keep_conflicts\")elseif key==\"x\"then applyImportAction(\"replace_conflicts\")elseif key==\"enter\"then applyImportAction(\"apply_import\")end elseif event==\"mouse_click\"and a==1 then local target=hit(layout.targets,b,c)if target and target.action==\"select_recipe\"then importReview.selected=target.index elseif target then importArmed=target.action end elseif event==\"mouse_drag\"and a==1 then local target=hit(layout.targets,b,c);importArmed=target and target.action or nil elseif event==\"mouse_up\"and a==1 then local target=hit(layout.targets,b,c)if target and target.action==\"select_recipe\"then importReview.selected=target.index;importSelection(importReview.review.rows[target.index])elseif target and target.action==importArmed then applyImportAction(target.action)end importArmed=nil end elseif modal then if event==\"char\"and modal.kind~=\"cancel_confirm\"then modal.value=(modal.replaceOnInput and\"\"or modal.value)..a;modal.replaceOnInput=false elseif event==\"paste\"and modal.kind~=\"cancel_confirm\"then modal.value=(modal.replaceOnInput and\"\"or modal.value)..tostring(a):gsub(\"[\\r\\n]\",\"\");modal.replaceOnInput=false elseif event==\"key\"then local key=keys.getName(a)if key==\"enter\"then finishModal(s,rows)elseif key==\"backspace\"then if modal.kind==\"cancel_confirm\"or#modal.value==0 then modal=nil else modal.value=modal.value:sub(1,-2);modal.replaceOnInput=false end end elseif event==\"mouse_up\"and a==1 then local _,h=term.getSize()if modal.kind==\"cancel_confirm\"then if c==h-1 and b>=2 and b<=17 then finishModal(s,rows)elseif c==h-1 then modal=nil end elseif c==h and b>=2 and b<=7 then finishModal(s,rows)elseif c==h and b>=9 then modal=nil end end elseif event==\"key\"then local key=keys.getName(a)if key==\"backspace\"or key==\"q\"then return elseif key==\"tab\"then focus=focus==\"stock\"and\"cart\"or(focus==\"cart\"and\"actions\"or\"stock\")elseif key==\"up\"then if focus==\"stock\"then selectedStock=math.max(1,selectedStock-1)elseif focus==\"cart\"then selectedCart=math.max(1,selectedCart-1)end elseif key==\"down\"then if focus==\"stock\"then selectedStock=math.min(math.max(1,#rows),selectedStock+1)elseif focus==\"cart\"then selectedCart=math.min(math.max(1,#s.cart),selectedCart+1)end elseif key==\"left\"and focus==\"actions\"then buttonIndex=math.max(1,buttonIndex-1)elseif key==\"right\"and focus==\"actions\"then buttonIndex=math.min(#layout.actions,buttonIndex+1)elseif key==\"s\"then applyAction(\"search\",s,rows)elseif key==\"f\"then applyAction(\"filter\",s,rows)elseif key==\"r\"then applyAction(\"refresh\",s,rows)elseif key==\"space\"then applyAction(\"mode\",s,rows)elseif key==\"d\"then applyAction(\"destination\",s,rows)elseif key==\"o\"then applyAction(\"submit\",s,rows)elseif key==\"c\"then applyAction(\"cancel\",s,rows)elseif key==\"delete\"then applyAction(\"delete\",s,rows)elseif key==\"enter\"then local action=focus==\"actions\"and layout.actions[buttonIndex]and layout.actions[buttonIndex].action or(focus==\"stock\"and\"add\"or\"edit\")if action and applyAction(action,s,rows)==\"exit\"then return end end elseif event==\"mouse_click\"and a==1 then local target=hit(layout.targets,b,c)if target then armedAction=target.action elseif c>=layout.top and c<=layout.bottom then local index=c-layout.top+1 if b<=layout.split and rows[index]then focus,selectedStock=\"stock\",index local now=os.epoch(\"utc\")if lastRowClick and lastRowClick.side==\"stock\"and lastRowClick.index==index and now-lastRowClick.at<=450 then startModal(\"add\",\"Add \"..rows[index].name..\" quantity\",\"1\",true);lastRowClick=nil else lastRowClick={side=\"stock\",index=index,at=now}end elseif b>layout.split and s.cart[index]then focus,selectedCart=\"cart\",index local now=os.epoch(\"utc\")if lastRowClick and lastRowClick.side==\"cart\"and lastRowClick.index==index and now-lastRowClick.at<=450 then startModal(\"edit\",\"Set \"..s.cart[index].name..\" quantity\",s.cart[index].count,true);lastRowClick=nil else lastRowClick={side=\"cart\",index=index,at=now}end end end elseif event==\"mouse_drag\"and a==1 then local target=hit(layout.targets,b,c);armedAction=target and target.action or nil elseif event==\"mouse_up\"and a==1 then local target=hit(layout.targets,b,c)if target and target.action==armedAction then if applyAction(target.action,s,rows)==\"exit\"then return end end armedAction=nil elseif event==\"file_transfer\"then feedback=\"Recipe packs must be imported on the selected Recipe Server\"end end",
  ["ceetos/bin/ceetshareall.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local peripherals=require(\"ceetos.lib.peripherals\")local mode=(...)==\"master\"and\"master\"or\"peer\"auth.require(\"operator\")print(\"Shared \"..peripherals.shareAll(mode)..\" peripherals (\"..mode..\")\")",
  ["ceetos/bin/ceetunshareall.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local peripherals=require(\"ceetos.lib.peripherals\")auth.require(\"operator\")print(\"Unshared \"..peripherals.unshareAll()..\" peripherals\")",
}
local CEETOS_RELEASE_INSTALLER = true
local CEETOS_PROFILE = "desktop"

-- `wget --install` may run inside a deliberately restricted CeetShell child
-- environment. Self-cleaning is optional there: never require the shell API
-- merely to install the runtime.
local args = { ... }
local automated = args[1] == "--ceetos-apply"
local self = shell and shell.getRunningProgram and shell.getRunningProgram()
local cache = "/ceetos-updates/release.lua"
-- A signed release launched directly is retained outside the replaceable
-- runtime tree for verified recovery. Development installers deliberately do
-- not become propagation sources. Do this before removing the downloaded
-- program itself, but avoid copying the cache onto itself during recovery.
if CEETOS_RELEASE_INSTALLER and not automated and self and fs.exists(self) and self ~= cache then
  if not fs.exists("/ceetos-updates") then fs.makeDir("/ceetos-updates") end
  if fs.exists(cache) then fs.delete(cache) end
  pcall(fs.copy, self, cache)
end
if self and self ~= cache and fs.exists(self) then pcall(fs.delete, self) end

local bytes = 0
for _, contents in pairs(files) do bytes = bytes + #contents end
print("CeetOS " .. CEETOS_PROFILE .. " installer (" .. bytes .. " bytes)")
if not automated then
  print("This will replace /ceetos and /startup.lua. Continue? [y/N]")
  if read():lower() ~= "y" then print("Cancelled."); return end
end

local backup, migrate, migrationData = nil, false, {}
-- Clean stale full-tree backups from older installers before measuring space.
for _, name in ipairs(fs.list(".")) do
  if name:match("^ceetos%.backup") or name == "ceetos-migrate" then fs.delete(name) end
end
-- Development transactions can be much larger than the runtime. Preserve
-- only the reconnect record; staged payloads, test data, and rollback trees
-- are disposable and must not prevent an install from starting.
for _, name in ipairs({ "staging", "backups", "test-data" }) do
  local path = fs.combine("ceetos-dev", name)
  if fs.exists(path) then fs.delete(path) end
end
if fs.exists("ceetos") then
  if automated then migrate = true else
    print("Migrate saved CeetOS configuration? [Y/n]")
    local choice = read():lower()
    migrate = choice ~= "n" and choice ~= "no"
  end
  -- Copy only durable configuration to a small migration area, then remove
  -- the old tree before writing the new one. This works on nearly-full CC
  -- drives where retaining a complete backup would make installation fail.
  if migrate then
    local durable = { "users.lua", "network.lua", "shares.lua", "config.lua", "owner-service.lua", "seen-peers.lua", "jobs.lua", "job-workers.lua", "crafting-state.lua", "auth-authority.lua", "auth-authority-state.lua", "auth-directory.lua", "auth-session.lua", "cloud.lua", "cloud-state.lua" }
    if CEETOS_PROFILE == "recipe-server" then durable[#durable + 1] = "recipes.lua" end
    for _, name in ipairs(durable) do
      local source = fs.combine("ceetos/data", name)
      if fs.exists(source) then
        local input = fs.open(source, "r")
        if input then migrationData[name] = input.readAll(); input.close() end
      end
    end
  end
  fs.delete("ceetos")
end

for path, contents in pairs(files) do
  local parent = fs.getDir(path)
  if parent ~= "" and not fs.exists(parent) then fs.makeDir(parent) end
  local handle = assert(fs.open(path, "w"))
  handle.write(contents)
  handle.close()
end

if migrate then
  -- Preserve durable configuration, but never preserve an active session,
  -- temporary recovery window, remote result cache, or stale audit entries.
  local durable = { "users.lua", "network.lua", "shares.lua", "config.lua", "owner-service.lua", "seen-peers.lua", "jobs.lua", "job-workers.lua", "crafting-state.lua", "auth-authority.lua", "auth-authority-state.lua", "auth-directory.lua", "auth-session.lua", "cloud.lua", "cloud-state.lua" }
  if CEETOS_PROFILE == "recipe-server" then durable[#durable + 1] = "recipes.lua" end
  for _, name in ipairs(durable) do
    local contents = migrationData[name]
    if contents then
      if not fs.exists("ceetos/data") then fs.makeDir("ceetos/data") end
      local output = assert(fs.open(fs.combine("ceetos/data", name), "w"))
      output.write(contents); output.close()
    end
  end
  print("Saved CeetOS configuration migrated. Please log in again.")
end

-- The installer is intentionally transactional while it runs, but retaining
-- a full tree backup after a successful install quickly exhausts small CC
-- drives. Durable data has already been copied, so remove the old tree.
if automated then
  -- The root startup launcher sees this marker and boots the freshly written
  -- runtime rather than replaying the installer. The runtime verifies its
  -- loaded version before clearing it.
  if not fs.exists("/ceetos-updates") then fs.makeDir("/ceetos-updates") end
  local marker = fs.open("/ceetos-updates/pending.lua", "w")
  if marker then marker.write("return {state='verify',version=" .. "0.16.3" .. ",profile=" .. "\"desktop\"" .. "}"); marker.close() end
  print("CeetOS installed. Rebooting to verify the update.")
  os.reboot()
end
print("CeetOS installed. Reboot to start it.")

-- CEETOS_RELEASE_PAYLOAD_END
