@@ -24,6 +24,7 @@ import {
2424 NotConfiguredError ,
2525 NotFoundError ,
2626 PreconditionFailedError ,
27+ type ActAs ,
2728 type Binding ,
2829 type DatamateRef ,
2930 type MatchedIdentifier ,
@@ -41,7 +42,7 @@ import {
4142 resolveWorkspaceWebUrl ,
4243 type HandoffResult ,
4344} from "@/altimate/workspace/browser-handoff"
44- import { accountDigest , recordApprovedBinding } from "@/altimate/workspace/state"
45+ import { accountDigest , digestOf , recordApprovedBinding } from "@/altimate/workspace/state"
4546import type { SeedOutcome } from "@/altimate/workspace/memory-backfill"
4647import {
4748 confirmsNamesake ,
@@ -243,11 +244,19 @@ export const LinkCommand = cmd({
243244 )
244245 }
245246
247+ // One credential for the list, the owner lookup and the bind: workspace and user ids are
248+ // per tenant, so a switch while the picker is open must not mix two accounts.
249+ const actAs = await WorkspaceApi . captureCredentials ( )
250+ if ( ! actAs ) {
251+ prompts . log . error ( "Could not read your Altimate credentials. Check /connect and try again." )
252+ process . exitCode = 1
253+ return
254+ }
246255 const spin = prompts . spinner ( )
247256 spin . start ( "Loading workspaces..." )
248257 let list : DatamateRef [ ]
249258 try {
250- list = await WorkspaceApi . listDatamates ( )
259+ list = await WorkspaceApi . listDatamates ( actAs )
251260 } catch ( err ) {
252261 spin . stop ( "Could not load workspaces." , 1 )
253262 prompts . log . error ( err instanceof Error ? err . message : String ( err ) )
@@ -263,7 +272,7 @@ export const LinkCommand = cmd({
263272 // Listed workspaces already named what a quick create would use. The picker opens on
264273 // the caller's own (never a colleague's), and creating another namesake is confirmed.
265274 // Without a user id nothing is preselected; the confirmation still applies.
266- const userId = await WorkspaceApi . whoami ( ) . catch ( ( ) => undefined )
275+ const userId = await WorkspaceApi . whoami ( actAs ) . catch ( ( ) => undefined )
267276 const namesakes = findNamesakes ( list , autoName , userId )
268277 // Sanitized once here so every downstream display (the picker message,
269278 // the "Kept" outro, hyperlink()'s own text) is covered — hyperlink()
@@ -381,7 +390,7 @@ export const LinkCommand = cmd({
381390 return
382391 }
383392
384- await bindOrRebind ( identifier , targetId , existing , preCheckOk , args . directory )
393+ await bindOrRebind ( identifier , targetId , existing , preCheckOk , args . directory , actAs )
385394 } ,
386395} )
387396
@@ -712,12 +721,13 @@ async function bindOrRebind(
712721 existing : ProjectBindingLookup | null ,
713722 preCheckOk : boolean ,
714723 directory : string ,
724+ /** The credential the picker's list was read as. The bind runs as it, and the seed refuses
725+ * (account-changed) if the configured account switches mid-way. */
726+ actAs : ActAs ,
715727) : Promise < void > {
716- // The account this bind acts as; the seed refuses (account-changed) if it switches mid-way.
717- // Unreadable credentials cannot link anyway, and must not leave the bind unguarded.
718- const linkAccount = await accountDigest ( )
719- if ( linkAccount === null ) {
720- prompts . log . error ( "Could not read your Altimate credentials, so nothing was linked. Check /connect and try again." )
728+ const linkAccount = digestOf ( actAs )
729+ if ( ( await accountDigest ( ) ) !== linkAccount ) {
730+ prompts . log . error ( "Your Altimate account changed since the list was loaded, so nothing was linked. Re-run `altimate-code link`." )
721731 process . exitCode = 1
722732 return
723733 }
@@ -732,13 +742,14 @@ async function bindOrRebind(
732742 targetDatamateId,
733743 expectedCurrentDatamateId : existing . datamate . id ,
734744 matchedBy : existing . matchedBy ,
745+ actAs,
735746 } )
736747 } else {
737748 // No known binding OR pre-check failed. Try bindExisting first — if the
738749 // pre-check missed a real binding, the server will 409, and we retry as
739750 // rebind when we're allowed to. (m10)
740751 try {
741- res = await WorkspaceApi . bindExisting ( targetDatamateId , identifier )
752+ res = await WorkspaceApi . bindExisting ( targetDatamateId , identifier , actAs )
742753 } catch ( err ) {
743754 // A teammate's private workspace is not a pre-check race: rebinding it only fails
744755 // again (forbidden), and would hide the explanation the outer handler gives.
@@ -767,11 +778,13 @@ async function bindOrRebind(
767778 res = await WorkspaceApi . rebindByPath ( {
768779 projectPath : conflictPath ,
769780 targetDatamateId,
781+ actAs,
770782 } )
771783 } else if ( conflictRemote ) {
772784 res = await WorkspaceApi . rebindByRemote ( {
773785 remote : conflictRemote ,
774786 targetDatamateId,
787+ actAs,
775788 } )
776789 } else {
777790 // Server didn't tell us which identifier owned the conflict —
@@ -781,10 +794,12 @@ async function bindOrRebind(
781794 ? await WorkspaceApi . rebindByRemote ( {
782795 remote : identifier . repoRemote ,
783796 targetDatamateId,
797+ actAs,
784798 } )
785799 : await WorkspaceApi . rebindByPath ( {
786800 projectPath : identifier . projectPath ! ,
787801 targetDatamateId,
802+ actAs,
788803 } )
789804 }
790805 rebindSpin . stop ( `Re-linked to "${ stripControlChars ( res . binding . datamate_name ) } ".` )
@@ -871,19 +886,22 @@ async function rebindByMatchedIdentifier(input: {
871886 targetDatamateId : number
872887 expectedCurrentDatamateId : number
873888 matchedBy : MatchedIdentifier
889+ actAs ?: ActAs
874890} ) {
875891 if ( input . matchedBy === "remote" && input . identifier . repoRemote ) {
876892 return WorkspaceApi . rebindByRemote ( {
877893 remote : input . identifier . repoRemote ,
878894 targetDatamateId : input . targetDatamateId ,
879895 expectedCurrentDatamateId : input . expectedCurrentDatamateId ,
896+ actAs : input . actAs ,
880897 } )
881898 }
882899 if ( input . matchedBy === "path" && input . identifier . projectPath ) {
883900 return WorkspaceApi . rebindByPath ( {
884901 projectPath : input . identifier . projectPath ,
885902 targetDatamateId : input . targetDatamateId ,
886903 expectedCurrentDatamateId : input . expectedCurrentDatamateId ,
904+ actAs : input . actAs ,
887905 } )
888906 }
889907 throw new Error (
0 commit comments