@@ -11,7 +11,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1111const TOKEN = / ^ b l z _ [ A - Z a - z 0 - 9 _ - ] { 43 } $ / ;
1212const CARD_ID = / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 - ] { 2 , 62 } $ / ;
1313const DEFAULT_ORIGIN = "https://blaze.pascal.app" ;
14- export const CLIENT_VERSION = "0.4.1 " ;
14+ export const CLIENT_VERSION = "0.4.2 " ;
1515export const CLIENT_CONTRACT = 1 ;
1616export const CLIENT_TOOLS = [ "claude" , "codex" , "opencode" , "cursor" , "openclaw" , "agent" ] ;
1717const RELEASE_FILES = [ "SKILL.md" , "blaze-client.mjs" ] ;
@@ -645,13 +645,14 @@ async function locked(path, work) {
645645export function createLifecycle ( { tool, home = homedir ( ) , origin, helperPath = fileURLToPath ( import . meta. url ) , fetchImpl = fetch } ) {
646646 const paths = toolPaths ( tool , home ) ;
647647 const invokedRoot = resolve ( dirname ( helperPath ) ) ;
648- // Hosts can discover another host's global copy. Only an already recorded
649- // direct bundle (or its interrupted transaction) can establish ownership.
648+ // Hosts can discover another host's global copy. Only a completed direct
649+ // ownership record binds that copy to an origin. The original host must
650+ // recover a first install interrupted before that record was committed.
650651 // Credentials and receipts still belong to the invoking tool's state directory.
651652 if ( invokedRoot !== resolve ( paths . root ) && CLIENT_TOOLS . some ( name => resolve ( toolPaths ( name , home ) . root ) === invokedRoot ) ) {
652653 const recordedState = join ( home , ".config/blaze/bundles" , sha256 ( invokedRoot ) . slice ( 0 , 32 ) ) ;
653654 homePath ( home , invokedRoot ) ; homePath ( home , recordedState ) ;
654- if ( pathStat ( join ( recordedState , "installation.json" ) ) || pathStat ( join ( recordedState , "transaction.json" ) ) ) paths . root = invokedRoot ;
655+ if ( pathStat ( join ( recordedState , "installation.json" ) ) ) paths . root = invokedRoot ;
655656 }
656657 const base = trustedOrigin ( origin ?? readToolCredential ( tool , home , false ) . origin ) ;
657658 const bundleState = join ( home , ".config/blaze/bundles" , sha256 ( resolve ( paths . root ) ) . slice ( 0 , 32 ) ) ;
@@ -762,10 +763,12 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
762763 }
763764 function recover ( ) {
764765 const journal = loadRequiredIfPresent ( journalPath ) ; if ( ! journal ) return ;
765- exactKeys ( journal , new Set ( [ "version" , "id" , "release" , "prior" ] ) , "Activation journal" ) ;
766- if ( journal . version !== 1 || ! UUID . test ( journal . id ?? "" ) ) throw new Error ( "Invalid activation journal" ) ;
766+ exactKeys ( journal , new Set ( journal . version === 2 ? [ "version" , "id" , "origin" , "release" , "prior" ] : [ "version" , "id" , "release" , "prior" ] ) , "Activation journal" ) ;
767+ if ( ! [ 1 , 2 ] . includes ( journal . version ) || ! UUID . test ( journal . id ?? "" ) ) throw new Error ( "Invalid activation journal" ) ;
767768 const next = validateRelease ( journal . release , base ) ;
768769 const prior = validateMetadata ( journal . prior ) ;
770+ const recordedOrigin = journal . version === 2 ? trustedOrigin ( journal . origin ) : prior ?. origin ;
771+ if ( recordedOrigin !== base ) throw new Error ( "Interrupted installation has no matching recorded service origin; preserve its state for recovery" ) ;
769772 const stage = join ( bundleState , "staging" , journal . id ) , backup = join ( bundleState , "backups" , journal . id ) ;
770773 homePath ( home , stage ) ; homePath ( home , backup ) ; homePath ( home , paths . root ) ;
771774 if ( existsSync ( paths . root ) && verifyBundle ( paths . root , next , false , true ) ) {
@@ -842,7 +845,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
842845 if ( syntax . status !== 0 ) throw new Error ( "Release client failed syntax validation" ) ;
843846 const credential = await setup ( ) ;
844847 migrateReceipts ( ) ;
845- save ( journalPath , { version :1 , id, release :next , prior} ) ;
848+ save ( journalPath , { version :2 , id, origin : base , release :next , prior} ) ;
846849 if ( existsSync ( paths . root ) ) renameSync ( paths . root , backup ) ;
847850 mkdirSync ( dirname ( paths . root ) , { recursive :true , mode :0o700 } ) ;
848851 renameSync ( stage , paths . root ) ;
@@ -885,7 +888,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
885888 if ( ! verifyBundle ( paths . root , meta . release ) || ! verifyBundle ( backup , previous ) ) throw new Error ( "Rollback bundle was modified" ) ;
886889 const id = randomUUID ( ) , stage = join ( bundleState , "staging" , id ) ;
887890 ensurePrivateDir ( dirname ( stage ) ) ; renameSync ( backup , stage ) ;
888- save ( journalPath , { version :1 , id, release :previous , prior :{ ...meta , pin :previous . version } } ) ;
891+ save ( journalPath , { version :2 , id, origin : base , release :previous , prior :{ ...meta , pin :previous . version } } ) ;
889892 const currentBackup = join ( bundleState , "backups" , id ) ; renameSync ( paths . root , currentBackup ) ; renameSync ( stage , paths . root ) ; recover ( ) ;
890893 return { version :previous . version , activation :"rolled_back" , reload_required :true , pin :previous . version } ;
891894 } ) ;
0 commit comments